From 4cc22828fd712b230a42952ecd3c0f3f243f4f26 Mon Sep 17 00:00:00 2001 From: Choi wontak <76509639+RoundTable02@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:22:02 +0900 Subject: [PATCH 01/15] =?UTF-8?q?feat:=20=EA=B8=B0=EC=A1=B4=20=EC=BB=A4?= =?UTF-8?q?=ED=94=8C=20=EC=97=B0=EB=8F=99=20=EA=B4=80=EB=A0=A8=20API=20Dep?= =?UTF-8?q?recated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/API-CHANGES-ONBOARDING-PROFILE.md | 19 +++--- docs/API-DEPRECATION-COUPLE-LINKING.md | 59 +++++++++++++++++++ .../in/web/controller/CoupleController.java | 12 +++- .../in/web/controller/MemberController.java | 10 +++- .../in/web/controller/SignUpController.java | 8 +-- .../adaptor/in/web/docs/SwaggerResponses.java | 26 +++++--- .../port/in/member/SignUpUseCase.java | 4 +- .../service/member/SignUpService.java | 4 +- .../cmc/malmo/domain/model/member/Member.java | 15 +---- 10 files changed, 110 insertions(+), 49 deletions(-) create mode 100644 docs/API-DEPRECATION-COUPLE-LINKING.md diff --git a/README.md b/README.md index 069cd731..9058b0e2 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ MZ세대는 연인과의 갈등 원인으로 '의사소통 방식'과 '성향 ### 1. 애착 유형 진단 - ECR 검사 문항 기반 애착 유형 진단 -- 커플 연동으로 서로의 결과 공유 및 AI 상담에 활용 +- 사용자가 직접 입력한 커플/상대 정보 기반으로 결과를 해석하고 AI 상담에 활용 ### 2. AI 갈등 상담 diff --git a/docs/API-CHANGES-ONBOARDING-PROFILE.md b/docs/API-CHANGES-ONBOARDING-PROFILE.md index 30182382..c1b4582d 100644 --- a/docs/API-CHANGES-ONBOARDING-PROFILE.md +++ b/docs/API-CHANGES-ONBOARDING-PROFILE.md @@ -2,11 +2,14 @@ ## 개요 -온보딩 및 멤버 프로필에 다음 3개 필드가 추가되었습니다: +멤버 프로필에는 다음 3개 필드가 추가되었습니다: - `relationshipStatus` - 연애 상태 (Enum) - `personalityType` - 본인 MBTI (String) - `otherPersonalityType` - 상대방 MBTI (String) +추가로, 커플 초대 코드 기반 연동 플로우는 deprecated 되었고 신규 사용자 플로우는 직접 입력 방식으로 전환되었습니다. +단, `POST /members/onboarding`에서는 이제 `relationshipStatus`만 받고 `personalityType`, `otherPersonalityType`는 받지 않습니다. + --- ## 신규 Enum @@ -40,8 +43,6 @@ { "nickname": "닉네임", "relationshipStatus": "IN_RELATIONSHIP", - "personalityType": "INTJ", - "otherPersonalityType": "ENFP", "terms": [ { "termsId": 1, "isAgreed": true } ] @@ -52,10 +53,11 @@ |------|------|-----------|------| | `nickname` | String | 필수 | 닉네임 (1-10자, 한글/영문/숫자) | | `relationshipStatus` | Enum | 선택 | 연애 상태 | -| `personalityType` | String | 선택 | 본인 MBTI | -| `otherPersonalityType` | String | 선택 | 상대방 MBTI | | `terms` | Array | 필수 | 약관 동의 목록 | +> **Note:** 커플/상대 관련 정보는 초대 코드 연동이 아니라 사용자의 직접 입력을 기준으로 관리합니다. +> **Note:** `personalityType`, `otherPersonalityType`는 회원가입 요청에서는 제거되었고, 필요 시 `PATCH /members`에서 관리합니다. + --- ### 2. GET /members (멤버 정보 조회) @@ -104,6 +106,7 @@ | `otherPersonalityType` | String (nullable) | 상대방 MBTI | > **Note:** 기존 사용자의 경우 위 필드들이 `null`로 반환될 수 있습니다. +> **Note:** 응답의 `inviteCode`, `isCouple`, `startLoveDate`는 레거시 커플 연동 흐름과 연결된 필드이므로 신규 클라이언트 플로우의 기준으로 사용하지 않는 것을 권장합니다. --- @@ -163,7 +166,7 @@ ### 온보딩 화면 업데이트 1. 닉네임 입력 후 추가 정보 입력 화면 구성 2. 연애 상태 선택 (3가지 옵션) -3. MBTI 입력 (본인/상대방) +3. MBTI 입력은 온보딩 이후 프로필 수정 플로우로 이동 ### 프로필 수정 화면 업데이트 1. 기존 닉네임 수정 기능 유지 @@ -178,7 +181,7 @@ - `Member.java` - 3개 필드 추가, `signUp()` 오버로드, `updateMemberProfile()` 확장 ### Application Layer -- `SignUpUseCase.java` - SignUpCommand에 3개 필드 추가 +- `SignUpUseCase.java` - SignUpCommand에 연애 상태 필드 추가 - `GetMemberUseCase.java` - MemberResponseDto에 3개 필드 추가 - `UpdateMemberUseCase.java` - Command/ResponseDto에 3개 필드 추가 - `SignUpService.java` - 새 signUp 메서드 호출 @@ -191,7 +194,7 @@ - `MemberMapper.java` - 양방향 매핑 확장 - `MemberPersistenceAdapter.java` - RepositoryDto에 3개 필드 추가 - `MemberRepositoryCustomImpl.java` - QueryDSL select 확장 -- `SignUpController.java` - RequestDto에 3개 필드 추가 +- `SignUpController.java` - RequestDto에 `relationshipStatus` 추가 - `MemberController.java` - UpdateMemberRequestDto에 3개 필드 추가 ### Enum diff --git a/docs/API-DEPRECATION-COUPLE-LINKING.md b/docs/API-DEPRECATION-COUPLE-LINKING.md new file mode 100644 index 00000000..3d282d6c --- /dev/null +++ b/docs/API-DEPRECATION-COUPLE-LINKING.md @@ -0,0 +1,59 @@ +# API 변경 사항 - 커플 연동 API Deprecated + +## 개요 + +커플 초대 코드 기반 연동 기능은 제거 예정입니다. +앞으로는 사용자가 커플 및 상대 정보를 직접 입력하는 방식으로 전환합니다. + +이번 변경에서는 기존 API를 즉시 삭제하지 않고 `Deprecated` 상태로 전환하여 클라이언트가 점진적으로 마이그레이션할 수 있도록 합니다. + +--- + +## Deprecated 대상 API + +### 1. `POST /couples` + +- 상태: Deprecated +- 기존 역할: 초대 코드로 커플 연동 +- 변경 방향: 더 이상 신규 연동 플로우의 기준 API로 사용하지 않습니다. + +### 2. `GET /members/invite-code` + +- 상태: Deprecated +- 기존 역할: 현재 사용자의 커플 초대 코드 조회 +- 변경 방향: 초대 코드 기반 연결 흐름이 제거되므로 신규 클라이언트에서는 사용하지 않습니다. + +### 3. `DELETE /couples` + +- 상태: Deprecated +- 기존 역할: 연결된 커플 해제 +- 변경 방향: 커플 연동 모델 축소에 따라 제거 예정입니다. + +### 4. `PATCH /members/start-love-date` + +- 상태: Deprecated +- 기존 역할: 연동된 커플의 연애 시작일 수정 +- 변경 방향: 연애/커플 정보는 사용자 입력 기반 프로필 관리 방식으로 전환합니다. + +--- + +## 클라이언트 대응 가이드 + +- 신규 앱/웹 플로우에서는 위 4개 API를 호출하지 않아야 합니다. +- 커플 관련 정보는 사용자 입력 기반 프로필/온보딩 필드를 사용해 관리해야 합니다. +- Swagger 문서에서 위 API들은 `deprecated`로 표시됩니다. + +--- + +## 서버 반영 내용 + +- 컨트롤러 메서드에 `@Deprecated` 적용 +- OpenAPI `deprecated = true` 적용 +- 관련 Swagger 응답 스키마 설명에 deprecated 표기 추가 + +--- + +## 비고 + +현재 단계에서는 하위 호환성을 위해 엔드포인트 자체는 유지합니다. +실제 제거 시점에는 별도 릴리즈 노트와 함께 삭제 일정을 안내해야 합니다. diff --git a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/CoupleController.java b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/CoupleController.java index 57224d62..40feb8a0 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/CoupleController.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/CoupleController.java @@ -20,7 +20,7 @@ import org.springframework.security.core.userdetails.User; import org.springframework.web.bind.annotation.*; -@Tag(name = "커플 관리 API", description = "커플 연결 및 관리 관련 API") +@Tag(name = "커플 관리 API", description = "커플 연결 및 관리 관련 API (Deprecated)") @RestController @RequestMapping("/couples") @RequiredArgsConstructor @@ -31,7 +31,8 @@ public class CoupleController { @Operation( summary = "커플 연결", - description = "커플 초대코드를 사용하여 커플을 연결합니다. JWT 토큰이 필요합니다.", + description = "[Deprecated] 커플 초대코드를 사용하여 커플을 연결합니다. 커플 연동 기능은 제거 예정이며, 앞으로는 사용자가 커플 정보를 직접 입력하는 방식을 사용합니다. JWT 토큰이 필요합니다.", + deprecated = true, security = @SecurityRequirement(name = "Bearer Authentication") ) @ApiResponse( @@ -42,6 +43,7 @@ public class CoupleController { @ApiCommonResponses.RequireAuth @ApiCommonResponses.CoupleCode @PostMapping + @Deprecated public BaseResponse linkCouple( @AuthenticationPrincipal User user, @Valid @RequestBody CoupleLinkRequestDto requestDto @@ -55,7 +57,8 @@ public BaseResponse linkCouple( @Operation( summary = "커플 연결 끊기", - description = "연결된 커플을 끊습니다. JWT 토큰이 필요합니다.", + description = "[Deprecated] 연결된 커플을 끊습니다. 커플 연동 기능은 제거 예정이며, 앞으로는 사용자가 커플 정보를 직접 입력하는 방식을 사용합니다. JWT 토큰이 필요합니다.", + deprecated = true, security = @SecurityRequirement(name = "Bearer Authentication") ) @ApiResponse( @@ -66,6 +69,7 @@ public BaseResponse linkCouple( @ApiCommonResponses.RequireAuth @ApiCommonResponses.OnlyCouple @DeleteMapping + @Deprecated public BaseResponse unlinkCouple( @AuthenticationPrincipal User user ) { @@ -80,6 +84,8 @@ public BaseResponse unlinkCouple( @Data + @Deprecated + @Schema(description = "[Deprecated] 커플 연결 요청 DTO") public static class CoupleLinkRequestDto { @NotBlank(message = "초대코드는 필수 입력값입니다.") @Size(min = 6, max = 8, message = "커플 코드는 6 ~ 8자리여야 합니다.") 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 92f36274..cdb6d63c 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 @@ -142,7 +142,8 @@ public BaseResponse> upda @Operation( summary = "사용자 초대 코드 조회", - description = "현재 로그인된 사용자의 초대 코드를 조회합니다. JWT 토큰이 필요합니다.", + description = "[Deprecated] 현재 로그인된 사용자의 초대 코드를 조회합니다. 커플 연동 기능은 제거 예정이며, 앞으로는 사용자가 커플 정보를 직접 입력하는 방식을 사용합니다. JWT 토큰이 필요합니다.", + deprecated = true, security = @SecurityRequirement(name = "Bearer Authentication") ) @ApiResponse( @@ -152,6 +153,7 @@ public BaseResponse> upda ) @ApiCommonResponses.RequireAuth @GetMapping("/invite-code") + @Deprecated public BaseResponse getMemberInviteCode( @AuthenticationPrincipal User user ) { @@ -221,7 +223,8 @@ public BaseResponse registerLoveType( @Operation( summary = "연애 시작일 변경", - description = "커플로 연동된 사용자의 연애 시작일을 변경합니다. 커플이 아닌 사용자는 사용할 수 없습니다. JWT 토큰이 필요합니다.", + description = "[Deprecated] 커플로 연동된 사용자의 연애 시작일을 변경합니다. 커플 연동 기능은 제거 예정이며, 앞으로는 사용자가 커플 정보를 직접 입력하는 방식을 사용합니다. 커플이 아닌 사용자는 사용할 수 없습니다. JWT 토큰이 필요합니다.", + deprecated = true, security = @SecurityRequirement(name = "Bearer Authentication") ) @ApiResponse( @@ -232,6 +235,7 @@ public BaseResponse registerLoveType( @ApiCommonResponses.RequireAuth @ApiCommonResponses.OnlyCouple @PatchMapping("/start-love-date") + @Deprecated public BaseResponse updateStartLoveDate( @AuthenticationPrincipal User user, @Valid @RequestBody UpdateStartLoveDateRequestDto requestDto @@ -261,6 +265,8 @@ public static class UpdateMemberTermsRequestDto { } @Data + @Deprecated + @Schema(description = "[Deprecated] 연애 시작일 변경 요청 DTO") public static class UpdateStartLoveDateRequestDto { @NotNull(message = "시작일은 필수 입력값입니다.") @PastOrPresent(message = "시작일은 오늘 또는 과거 날짜여야 합니다.") 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 71b38407..00d273f5 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 @@ -32,7 +32,7 @@ public class SignUpController { @Operation( summary = "회원가입", - description = "인증된 사용자의 추가 정보를 입력받아 회원가입을 완료합니다. 연애 시작일은 커플 연동 시 자동으로 설정됩니다. JWT 토큰이 필요합니다.", + description = "인증된 사용자의 추가 정보와 연애 상태를 입력받아 회원가입을 완료합니다. MBTI 관련 정보는 회원가입 이후 프로필 수정에서 관리합니다. JWT 토큰이 필요합니다.", security = @SecurityRequirement(name = "Bearer Authentication") ) @ApiResponse( @@ -60,8 +60,6 @@ public BaseResponse signUp( .nickname(requestDto.getNickname()) .loveTypeId(requestDto.getLoveTypeId()) .relationshipStatus(requestDto.getRelationshipStatus()) - .personalityType(requestDto.getPersonalityType()) - .otherPersonalityType(requestDto.getOtherPersonalityType()) .build(); signUpUseCase.signUp(command); @@ -82,10 +80,6 @@ public static class SignUpRequestDto { private Long loveTypeId; private RelationshipStatus relationshipStatus; - - private String personalityType; - - private String otherPersonalityType; } @Data 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 92d0a7ee..73247224 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 @@ -109,18 +109,21 @@ public static class ReadMemberNotificationSuccessResponse extends BaseSwaggerRes } @Getter - @Schema(description = "멤버 초대 코드 성공 응답") + @Deprecated + @Schema(description = "[Deprecated] 멤버 초대 코드 성공 응답") public static class GetInviteCodeSuccessResponse extends BaseSwaggerResponse { } // 커플 관련 응답 @Getter - @Schema(description = "커플 연결 성공 응답") + @Deprecated + @Schema(description = "[Deprecated] 커플 연결 성공 응답") public static class CoupleLinkSuccessResponse extends BaseSwaggerResponse { } @Getter - @Schema(description = "커플 연결 끊기 성공 응답") + @Deprecated + @Schema(description = "[Deprecated] 커플 연결 끊기 성공 응답") public static class CoupleUnlinkSuccessResponse extends BaseSwaggerResponse { } @@ -141,7 +144,8 @@ public static class RegisterLoveTypeSuccessResponse extends BaseSwaggerResponse< } @Getter - @Schema(description = "연애 시작일 갱신 성공 응답") + @Deprecated + @Schema(description = "[Deprecated] 연애 시작일 갱신 성공 응답") public static class UpdateStartLoveDateSuccessResponse extends BaseSwaggerResponse { } @@ -348,14 +352,16 @@ public static class TermsData { } @Getter - @Schema(description = "커플 연결 응답 데이터") + @Deprecated + @Schema(description = "[Deprecated] 커플 연결 응답 데이터") public static class CoupleLinkData { @Schema(description = "생성된 커플 ID", example = "1") private Long coupleId; } @Getter - @Schema(description = "커플 연결 끊기 응답 데이터") + @Deprecated + @Schema(description = "[Deprecated] 커플 연결 끊기 응답 데이터") public static class CoupleUnlinkData { @Schema(description = "해제된 커플 ID", example = "1") private Long coupleId; @@ -387,7 +393,8 @@ public static class LoveTypeQuestionCalculationData { } @Getter - @Schema(description = "연애 시작일 갱신 응답 데이터") + @Deprecated + @Schema(description = "[Deprecated] 연애 시작일 갱신 응답 데이터") public static class UpdateStartLoveDateData { @Schema(description = "변경된 연애 시작일", example = "2023-01-15") private LocalDate startLoveDate; @@ -534,7 +541,8 @@ public static class TermsDetailsResponseData { } @Getter - @Schema(description = "초대 코드 응답 데이터") + @Deprecated + @Schema(description = "[Deprecated] 초대 코드 응답 데이터") public static class InviteCodeResponseData { private String coupleCode; } @@ -625,4 +633,4 @@ public static class GetChatRoomListResponse { @Schema(description = "채팅방 생성 시간", example = "2025-07-20T10:15:30") private LocalDateTime createdAt; } -} \ No newline at end of file +} 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 9b926adf..77172304 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 @@ -18,8 +18,6 @@ class SignUpCommand { private String nickname; private Long loveTypeId; private RelationshipStatus relationshipStatus; - private String personalityType; - private String otherPersonalityType; } @Data @@ -28,4 +26,4 @@ class TermsCommand { private Long termsId; private Boolean isAgreed; } -} \ No newline at end of file +} 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 eb3751c5..54e573e1 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 @@ -35,9 +35,7 @@ public void signUp(SignUpUseCase.SignUpCommand command) { Member member = memberQueryHelper.getMemberByIdOrThrow(MemberId.of(command.getMemberId())); member.signUp( command.getNickname(), - command.getRelationshipStatus(), - command.getPersonalityType(), - command.getOtherPersonalityType() + command.getRelationshipStatus() ); // 회원가입 전 애착 유형 검사를 진행했던 사용자인 경우 해당 정보를 가져와 덮어쓰기 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 ce2d2fc0..461aaa9b 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 @@ -133,20 +133,9 @@ public void signUp(String nickname, LocalDate startLoveDate) { this.memberState = MemberState.ALIVE; } - /** - * V2 회원가입 - startLoveDate 없이 회원가입 - * 커플 연동 후 별도로 연애 시작일을 설정합니다. - */ - public void signUp(String nickname) { - this.nickname = nickname; - this.memberState = MemberState.ALIVE; - } - - public void signUp(String nickname, RelationshipStatus relationshipStatus, String personalityType, String otherPersonalityType) { + public void signUp(String nickname, RelationshipStatus relationshipStatus) { this.nickname = nickname; this.relationshipStatus = relationshipStatus; - this.personalityType = personalityType; - this.otherPersonalityType = otherPersonalityType; this.memberState = MemberState.ALIVE; } @@ -222,4 +211,4 @@ public void linkCouple(CoupleId coupleId) { public void unlinkCouple() { this.coupleId = null; } -} \ No newline at end of file +} From 6ba0f94ac6e7ec9328ab822804a56b9f9ea7db0f Mon Sep 17 00:00:00 2001 From: Choi wontak <76509639+RoundTable02@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:51:32 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat:=20=EC=83=81=EB=8C=80=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9E=90=EC=8B=A0=EC=9D=98=20=EC=9C=A0=ED=98=95=EC=9D=84=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=ED=95=98=EB=8A=94=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/API-CHANGES-LOVETYPE-DATA.md | 55 +++++++ sqls/MM-180.sql | 26 ++++ .../malmo/adaptor/in/exception/ErrorCode.java | 2 + .../in/exception/GlobalExceptionHandler.java | 14 +- .../in/web/controller/MemberController.java | 125 ++++++++++++++-- .../in/web/controller/QuestionController.java | 22 ++- .../adaptor/in/web/docs/SwaggerResponses.java | 57 +++++--- .../adapter/MemberPersistenceAdapter.java | 26 ++-- .../entity/member/MemberEntity.java | 8 +- .../out/persistence/mapper/MemberMapper.java | 12 +- .../member/MemberRepositoryCustomImpl.java | 83 ++--------- .../PartnerProfileAlreadyExistsException.java | 7 + .../PartnerProfileNotFoundException.java | 7 + .../helper/member/MemberQueryHelper.java | 18 +-- .../member/CreatePartnerProfileUseCase.java | 26 ++++ .../port/in/member/GetMemberUseCase.java | 6 +- .../port/in/member/GetPartnerUseCase.java | 12 +- .../port/in/member/UpdateMemberUseCase.java | 9 +- .../member/UpdatePartnerProfileUseCase.java | 20 +++ .../out/chat/LoadChatRoomMetadataPort.java | 3 +- .../service/chat/ChatPromptBuilder.java | 10 +- .../service/member/MemberCommandService.java | 71 ++++++++- .../service/member/MemberInfoService.java | 15 +- .../cmc/malmo/domain/model/member/Member.java | 48 +++++-- .../value/type/PartnerLoveTypeCategory.java | 24 ++++ .../CoupleIntegrationTest.java | 8 +- .../MemberIntegrationTest.java | 136 ++++++++++-------- .../cmc/malmo/mapper/MemberMapperTest.java | 15 +- .../service/AppleNotificationServiceTest.java | 7 +- 29 files changed, 616 insertions(+), 256 deletions(-) create mode 100644 docs/API-CHANGES-LOVETYPE-DATA.md create mode 100644 sqls/MM-180.sql create mode 100644 src/main/java/makeus/cmc/malmo/application/exception/PartnerProfileAlreadyExistsException.java create mode 100644 src/main/java/makeus/cmc/malmo/application/exception/PartnerProfileNotFoundException.java create mode 100644 src/main/java/makeus/cmc/malmo/application/port/in/member/CreatePartnerProfileUseCase.java create mode 100644 src/main/java/makeus/cmc/malmo/application/port/in/member/UpdatePartnerProfileUseCase.java create mode 100644 src/main/java/makeus/cmc/malmo/domain/value/type/PartnerLoveTypeCategory.java diff --git a/docs/API-CHANGES-LOVETYPE-DATA.md b/docs/API-CHANGES-LOVETYPE-DATA.md new file mode 100644 index 00000000..6e3dbf1c --- /dev/null +++ b/docs/API-CHANGES-LOVETYPE-DATA.md @@ -0,0 +1,55 @@ +## 신규 API 스펙 +### `POST /members/partners` — 상대방 프로필 최초 등록 +**Request** +```ts +{ + mbti: string, + loveTypeCategory: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE' | null + // null = "모르겠어요" 선택 +} +``` +**Response** +```ts +{ + mbti: string, + loveTypeCategory: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE' | 'UNKNOWN', + description: string +} +``` +--- +### `PATCH /members/partners` — 상대방 프로필 수정 +**Request** +```ts +{ + mbti?: string, + loveTypeCategory?: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE' | null +} +``` +**Response** +```ts +{ + mbti: string, + loveTypeCategory: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE' | 'UNKNOWN', + description: string +} +``` +--- +### `PATCH /members` — 기존 수정 +기존 필드 유지, 아래 필드 추가 +```ts +{ + loveTypeCategory?: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE' +} +``` +--- +### `GET /members` 응답 필드 추가 +기존 응답 유지, 아래 필드 추가 +```ts +{ + mbti: string, // 내 MBTI + loveTypeCategory: enum, // 내 애착유형 + partnerMbti: string, // 상대 MBTI + partnerLoveTypeCategory: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE' | 'UNKNOWN' + // undefined = 미입력 / UNKNOWN = "모르겠어요" 선택됨 +} +``` \ No newline at end of file diff --git a/sqls/MM-180.sql b/sqls/MM-180.sql new file mode 100644 index 00000000..5ae72899 --- /dev/null +++ b/sqls/MM-180.sql @@ -0,0 +1,26 @@ +-- MM-180: Add direct partner profile fields to member_entity +-- Author: Codex +-- Date: 2026-03-16 + +ALTER TABLE member_entity +ADD COLUMN mbti VARCHAR(4); + +ALTER TABLE member_entity +ADD COLUMN partner_mbti VARCHAR(4); + +ALTER TABLE member_entity +ADD COLUMN partner_love_type_category VARCHAR(255); + +UPDATE member_entity +SET mbti = UPPER(personality_type) +WHERE personality_type IS NOT NULL + AND mbti IS NULL; + +UPDATE member_entity +SET partner_mbti = UPPER(other_personality_type) +WHERE other_personality_type IS NOT NULL + AND partner_mbti IS NULL; + +COMMENT ON COLUMN member_entity.mbti IS 'Member MBTI'; +COMMENT ON COLUMN member_entity.partner_mbti IS 'Partner MBTI entered directly by member'; +COMMENT ON COLUMN member_entity.partner_love_type_category IS 'Partner love type category: STABLE_TYPE, ANXIETY_TYPE, AVOIDANCE_TYPE, CONFUSION_TYPE, UNKNOWN'; diff --git a/src/main/java/makeus/cmc/malmo/adaptor/in/exception/ErrorCode.java b/src/main/java/makeus/cmc/malmo/adaptor/in/exception/ErrorCode.java index 50cd2382..0121f8f8 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/exception/ErrorCode.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/in/exception/ErrorCode.java @@ -26,6 +26,8 @@ public enum ErrorCode { NO_SUCH_BOOKMARK(HttpStatus.BAD_REQUEST, 40014, "북마크가 존재하지 않습니다."), BOOKMARK_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, 40015, "이미 북마크된 메시지입니다."), NO_SUCH_MESSAGE(HttpStatus.BAD_REQUEST, 40016, "메시지가 존재하지 않습니다."), + PARTNER_PROFILE_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, 40017, "이미 상대 프로필이 등록되어 있습니다."), + NO_SUCH_PARTNER_PROFILE(HttpStatus.BAD_REQUEST, 40018, "등록된 상대 프로필이 존재하지 않습니다."), // 401 Unauthorized UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 40100, "인증되지 않은 사용자입니다."), 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 149dc51d..80f7e11a 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 @@ -168,6 +168,18 @@ public ResponseEntity handleMessageNotFoundException(MessageNotFo return ErrorResponse.of(ErrorCode.NO_SUCH_MESSAGE); } + @ExceptionHandler({PartnerProfileAlreadyExistsException.class}) + public ResponseEntity handlePartnerProfileAlreadyExistsException(PartnerProfileAlreadyExistsException e) { + log.info("[GlobalExceptionHandler: handlePartnerProfileAlreadyExistsException 호출] {}", e.getMessage()); + return ErrorResponse.of(ErrorCode.PARTNER_PROFILE_ALREADY_EXISTS); + } + + @ExceptionHandler({PartnerProfileNotFoundException.class}) + public ResponseEntity handlePartnerProfileNotFoundException(PartnerProfileNotFoundException e) { + log.info("[GlobalExceptionHandler: handlePartnerProfileNotFoundException 호출] {}", e.getMessage()); + return ErrorResponse.of(ErrorCode.NO_SUCH_PARTNER_PROFILE); + } + /** @@ -204,4 +216,4 @@ public ResponseEntity handleMethodArgumentNotValidException(Metho 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/MemberController.java b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/MemberController.java index cdb6d63c..c48d0ccb 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 @@ -16,6 +16,8 @@ import makeus.cmc.malmo.adaptor.in.web.dto.BaseListResponse; import makeus.cmc.malmo.adaptor.in.web.dto.BaseResponse; import makeus.cmc.malmo.application.port.in.member.*; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; import makeus.cmc.malmo.domain.value.type.RelationshipStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.User; @@ -35,6 +37,8 @@ public class MemberController { private final GetPartnerUseCase getPartnerUseCase; private final GetInviteCodeUseCase getInviteCodeUseCase; private final UpdateMemberUseCase updateMemberUseCase; + private final CreatePartnerProfileUseCase createPartnerProfileUseCase; + private final UpdatePartnerProfileUseCase updatePartnerProfileUseCase; private final UpdateTermsAgreementUseCase updateTermsAgreementUseCase; private final UpdateMemberLoveTypeUseCase updateMemberLoveTypeUseCase; private final UpdateStartLoveDateUseCase updateStartLoveDateUseCase; @@ -62,8 +66,9 @@ public BaseResponse getMemberInfo( } @Operation( - summary = "커플 상대 정보 조회", - description = "현재 로그인된 멤버의 파트너 정보를 조회합니다. JWT 토큰이 필요합니다.", + summary = "상대 프로필 조회", + description = "[Deprecated] 현재 로그인된 멤버가 직접 입력한 상대 프로필을 조회합니다. 신규 클라이언트는 GET /members 응답의 partner 필드를 사용하세요. JWT 토큰이 필요합니다.", + deprecated = true, security = @SecurityRequirement(name = "Bearer Authentication") ) @ApiResponse( @@ -71,9 +76,9 @@ public BaseResponse getMemberInfo( description = "파트너 멤버 정보 조회 성공", content = @Content(schema = @Schema(implementation = SwaggerResponses.PartnerMemberInfoSuccessResponse.class)) ) - @ApiCommonResponses.OnlyCouple @ApiCommonResponses.RequireAuth @GetMapping("/partner") + @Deprecated public BaseResponse getPartnerMemberInfo( @AuthenticationPrincipal User user ) { @@ -103,12 +108,66 @@ public BaseResponse updateMember( .memberId(Long.valueOf(user.getUsername())) .nickname(requestDto.getNickname()) .relationshipStatus(requestDto.getRelationshipStatus()) - .personalityType(requestDto.getPersonalityType()) - .otherPersonalityType(requestDto.getOtherPersonalityType()) + .mbti(normalizeMbti(requestDto.getMbti())) + .loveTypeCategory(requestDto.getLoveTypeCategory()) .build(); return BaseResponse.success(updateMemberUseCase.updateMember(command)); } + @Operation( + summary = "상대 프로필 최초 등록", + description = "현재 로그인된 사용자가 상대방 MBTI와 애착 유형을 직접 입력합니다. JWT 토큰이 필요합니다.", + security = @SecurityRequirement(name = "Bearer Authentication") + ) + @ApiResponse( + responseCode = "200", + description = "상대 프로필 등록 성공", + content = @Content(schema = @Schema(implementation = SwaggerResponses.PartnerProfileSuccessResponse.class)) + ) + @ApiCommonResponses.RequireAuth + @PostMapping("/partners") + public BaseResponse createPartnerProfile( + @AuthenticationPrincipal User user, + @RequestBody @Valid CreatePartnerProfileRequestDto requestDto + ) { + CreatePartnerProfileUseCase.CreatePartnerProfileCommand command = + CreatePartnerProfileUseCase.CreatePartnerProfileCommand.builder() + .memberId(Long.valueOf(user.getUsername())) + .mbti(normalizeMbti(requestDto.getMbti())) + .loveTypeCategory(requestDto.getLoveTypeCategory()) + .build(); + + return BaseResponse.success(createPartnerProfileUseCase.createPartnerProfile(command)); + } + + @Operation( + summary = "상대 프로필 수정", + description = "현재 로그인된 사용자가 직접 입력한 상대 프로필을 수정합니다. JWT 토큰이 필요합니다.", + security = @SecurityRequirement(name = "Bearer Authentication") + ) + @ApiResponse( + responseCode = "200", + description = "상대 프로필 수정 성공", + content = @Content(schema = @Schema(implementation = SwaggerResponses.PartnerProfileSuccessResponse.class)) + ) + @ApiCommonResponses.RequireAuth + @PatchMapping("/partners") + public BaseResponse updatePartnerProfile( + @AuthenticationPrincipal User user, + @RequestBody @Valid UpdatePartnerProfileRequestDto requestDto + ) { + UpdatePartnerProfileUseCase.UpdatePartnerProfileCommand command = + UpdatePartnerProfileUseCase.UpdatePartnerProfileCommand.builder() + .memberId(Long.valueOf(user.getUsername())) + .mbti(normalizeMbti(requestDto.getMbti())) + .mbtiProvided(requestDto.isMbtiProvided()) + .loveTypeCategory(requestDto.getLoveTypeCategory()) + .loveTypeCategoryProvided(requestDto.isLoveTypeCategoryProvided()) + .build(); + + return BaseResponse.success(updatePartnerProfileUseCase.updatePartnerProfile(command)); + } + @Operation( summary = "사용자 약관 동의 수정", description = "현재 로그인된 사용자의 약관 동의 정보를 수정합니다. JWT 토큰이 필요합니다.", @@ -253,10 +312,56 @@ public static class UpdateMemberRequestDto { @Size(min = 1, max = 10, message = "닉네임은 1자 이상 10자 이하여야 합니다.") @Pattern(regexp = "^[가-힣a-zA-Z0-9]+$", message = "닉네임은 한글, 영문, 숫자만 사용 가능합니다.") private String nickname; - + private RelationshipStatus relationshipStatus; - private String personalityType; - private String otherPersonalityType; + + @Pattern(regexp = "^[a-zA-Z]{4}$", message = "MBTI는 영문 4자리여야 합니다.") + private String mbti; + + private LoveTypeCategory loveTypeCategory; + } + + @Data + public static class CreatePartnerProfileRequestDto { + @NotBlank(message = "상대 MBTI는 필수 입력값입니다.") + @Pattern(regexp = "^[a-zA-Z]{4}$", message = "MBTI는 영문 4자리여야 합니다.") + private String mbti; + + private PartnerLoveTypeCategory loveTypeCategory; + } + + public static class UpdatePartnerProfileRequestDto { + @Pattern(regexp = "^[a-zA-Z]{4}$", message = "MBTI는 영문 4자리여야 합니다.") + private String mbti; + private boolean mbtiProvided; + private PartnerLoveTypeCategory loveTypeCategory; + private boolean loveTypeCategoryProvided; + + public String getMbti() { + return mbti; + } + + public boolean isMbtiProvided() { + return mbtiProvided; + } + + public PartnerLoveTypeCategory getLoveTypeCategory() { + return loveTypeCategory; + } + + public boolean isLoveTypeCategoryProvided() { + return loveTypeCategoryProvided; + } + + public void setMbti(String mbti) { + this.mbti = mbti; + this.mbtiProvided = true; + } + + public void setLoveTypeCategory(PartnerLoveTypeCategory loveTypeCategory) { + this.loveTypeCategory = loveTypeCategory; + this.loveTypeCategoryProvided = true; + } } @Data @@ -296,4 +401,8 @@ public static class LoveTypeTestResult { private Integer score; } + private static String normalizeMbti(String mbti) { + return mbti == null ? null : mbti.toUpperCase(); + } + } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/QuestionController.java b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/QuestionController.java index ca5f1a89..ee0e7d56 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/QuestionController.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/QuestionController.java @@ -20,7 +20,7 @@ import org.springframework.security.core.userdetails.User; import org.springframework.web.bind.annotation.*; -@Tag(name = "오늘의 질문 API", description = "커플 오늘의 질문 API") +@Tag(name = "오늘의 질문 API", description = "오늘의 질문 API (Deprecated)") @Slf4j @RestController @RequestMapping("/questions") @@ -33,7 +33,8 @@ public class QuestionController { @Operation( summary = "오늘의 질문 조회", - description = "커플 오늘의 질문을 조회합니다. JWT 토큰이 필요합니다.", + description = "[Deprecated] 오늘의 질문 기능은 제거 예정입니다. JWT 토큰이 필요합니다.", + deprecated = true, security = @SecurityRequirement(name = "Bearer Authentication") ) @ApiResponse( @@ -43,6 +44,7 @@ public class QuestionController { ) @ApiCommonResponses.RequireAuth @GetMapping("/today") + @Deprecated public BaseResponse getTodayQuestion( @AuthenticationPrincipal User user ) { @@ -55,7 +57,8 @@ public BaseResponse getTodayQuestion( @Operation( summary = "과거 질문 조회", - description = "커플 오늘의 질문을 조회합니다. JWT 토큰이 필요합니다.", + description = "[Deprecated] 오늘의 질문 기능은 제거 예정입니다. JWT 토큰이 필요합니다.", + deprecated = true, security = @SecurityRequirement(name = "Bearer Authentication") ) @ApiResponse( @@ -66,6 +69,7 @@ public BaseResponse getTodayQuestion( @ApiCommonResponses.OnlyCouple @ApiCommonResponses.RequireAuth @GetMapping("/{level}") + @Deprecated public BaseResponse getQuestion( @AuthenticationPrincipal User user, @PathVariable int level) { @@ -79,7 +83,8 @@ public BaseResponse getQuestion( @Operation( summary = "질문 답변 조회", - description = "커플 질문 답변을 조회합니다. JWT 토큰이 필요합니다.", + description = "[Deprecated] 오늘의 질문 기능은 제거 예정입니다. JWT 토큰이 필요합니다.", + deprecated = true, security = @SecurityRequirement(name = "Bearer Authentication") ) @ApiResponse( @@ -90,6 +95,7 @@ public BaseResponse getQuestion( @ApiCommonResponses.OnlyOwner @ApiCommonResponses.RequireAuth @GetMapping("/{coupleQuestionId}/answers") + @Deprecated public BaseResponse getAnswers( @AuthenticationPrincipal User user, @PathVariable Long coupleQuestionId) { @@ -103,7 +109,8 @@ public BaseResponse getAnswers( @Operation( summary = "오늘의 질문 답변 등록", - description = "커플 오늘의 질문에 답변을 등록합니다. JWT 토큰이 필요합니다.", + description = "[Deprecated] 오늘의 질문 기능은 제거 예정입니다. JWT 토큰이 필요합니다.", + deprecated = true, security = @SecurityRequirement(name = "Bearer Authentication") ) @ApiResponse( @@ -113,6 +120,7 @@ public BaseResponse getAnswers( ) @ApiCommonResponses.RequireAuth @PostMapping("/today/answers") + @Deprecated public BaseResponse postAnswer( @AuthenticationPrincipal User user, @Valid @RequestBody AnswerRequestDto requestDto @@ -127,7 +135,8 @@ public BaseResponse postAnswer( @Operation( summary = "오늘의 질문 답변 수정", - description = "커플 오늘의 질문에 답변을 수정합니다. JWT 토큰이 필요합니다.", + description = "[Deprecated] 오늘의 질문 기능은 제거 예정입니다. JWT 토큰이 필요합니다.", + deprecated = true, security = @SecurityRequirement(name = "Bearer Authentication") ) @ApiResponse( @@ -137,6 +146,7 @@ public BaseResponse postAnswer( ) @ApiCommonResponses.RequireAuth @PatchMapping("/today/answers") + @Deprecated public BaseResponse updateAnswer( @AuthenticationPrincipal User user, @Valid @RequestBody AnswerRequestDto requestDto 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 73247224..7b34fc9c 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 @@ -83,6 +83,11 @@ public static class MemberInfoSuccessResponse extends BaseSwaggerResponse { } + @Getter + @Schema(description = "상대 프로필 등록/수정 성공 응답") + public static class PartnerProfileSuccessResponse extends BaseSwaggerResponse { + } + @Getter @Schema(description = "멤버 정보 수정 성공 응답") public static class UpdateMemberSuccessResponse extends BaseSwaggerResponse { @@ -278,33 +283,41 @@ public static class MemberData { @Schema(description = "연애 상태", example = "IN_RELATIONSHIP") private RelationshipStatus relationshipStatus; - @Schema(description = "성격 유형", example = "INTJ") - private String personalityType; + @Schema(description = "내 MBTI", example = "INTJ") + private String mbti; - @Schema(description = "상대방 성격 유형", example = "ENFP") - private String otherPersonalityType; + @Schema(description = "상대방 MBTI", example = "ENFP") + private String partnerMbti; + + @Schema(description = "상대방 애착 유형", example = "UNKNOWN") + private PartnerLoveTypeCategory partnerLoveTypeCategory; } @Getter - @Schema(description = "파트너 멤버 정보 응답 데이터") + @Deprecated + @Schema(description = "[Deprecated] 상대 프로필 조회 응답 데이터") public static class PartnerMemberData { - @Schema(description = "멤버 상태", example = "ALIVE") - private MemberState memberState; + @Schema(description = "상대방 MBTI", example = "ENFP") + private String mbti; - @Schema(description = "애착 유형", example = "STABLE_TYPE") - private LoveTypeCategory loveTypeCategory; + @Schema(description = "상대방 애착 유형", example = "UNKNOWN") + private PartnerLoveTypeCategory loveTypeCategory; - @Schema(description = "회피 비율", example = "0.3") - private float avoidanceRate; + @Schema(description = "애착 유형 설명", example = "모르겠어요") + private String description; + } - @Schema(description = "불안 비율", example = "0.2") - private float anxietyRate; + @Getter + @Schema(description = "상대 프로필 응답 데이터") + public static class PartnerProfileData { + @Schema(description = "상대방 MBTI", example = "ENFP") + private String mbti; - @Schema(description = "닉네임", example = "김영희") - private String nickname; + @Schema(description = "상대방 애착 유형", example = "UNKNOWN") + private PartnerLoveTypeCategory loveTypeCategory; - @Schema(description = "디데이 변경 이력 여부", example = "false") - private Boolean isStartLoveDateUpdated; + @Schema(description = "애착 유형 설명", example = "모르겠어요") + private String description; } @Getter @@ -313,8 +326,14 @@ public static class UpdateMemberData { @Schema(description = "닉네임", example = "홍길동") private String nickname; - @Schema(description = "이메일", example = "test@example.com") - private String email; + @Schema(description = "연애 상태", example = "IN_RELATIONSHIP") + private RelationshipStatus relationshipStatus; + + @Schema(description = "내 MBTI", example = "INTJ") + private String mbti; + + @Schema(description = "내 애착 유형", example = "STABLE_TYPE") + private LoveTypeCategory loveTypeCategory; } @Data 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 72af5421..757e5d71 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 @@ -19,6 +19,7 @@ import makeus.cmc.malmo.domain.value.id.InviteCodeValue; import makeus.cmc.malmo.domain.value.id.MemberId; import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; import makeus.cmc.malmo.domain.value.type.Provider; import makeus.cmc.malmo.domain.value.type.RelationshipStatus; import org.springframework.stereotype.Component; @@ -104,8 +105,9 @@ public static class MemberResponseRepositoryDto { private String nickname; private String email; private RelationshipStatus relationshipStatus; - private String personalityType; - private String otherPersonalityType; + private String mbti; + private String partnerMbti; + private PartnerLoveTypeCategory partnerLoveTypeCategory; public MemberQueryHelper.MemberInfoDto toDto(int totalChatRoomCount, int totalCoupleQuestionCount) { return MemberQueryHelper.MemberInfoDto.builder() @@ -120,8 +122,9 @@ public MemberQueryHelper.MemberInfoDto toDto(int totalChatRoomCount, int totalCo .totalChatRoomCount(totalChatRoomCount) .totalCoupleQuestionCount(totalCoupleQuestionCount) .relationshipStatus(relationshipStatus) - .personalityType(personalityType) - .otherPersonalityType(otherPersonalityType) + .mbti(mbti) + .partnerMbti(partnerMbti) + .partnerLoveTypeCategory(partnerLoveTypeCategory) .build(); } } @@ -129,21 +132,14 @@ public MemberQueryHelper.MemberInfoDto toDto(int totalChatRoomCount, int totalCo @Data @AllArgsConstructor public static class PartnerMemberRepositoryDto { - private String memberState; - private LoveTypeCategory loveTypeCategory; - private float avoidanceRate; - private float anxietyRate; - private String nickname; - private Boolean isStartLoveDateUpdated; + private String mbti; + private PartnerLoveTypeCategory loveTypeCategory; public MemberQueryHelper.PartnerMemberDto toDto() { return MemberQueryHelper.PartnerMemberDto.builder() - .memberState(memberState) + .mbti(mbti) .loveTypeCategory(loveTypeCategory) - .avoidanceRate(avoidanceRate) - .anxietyRate(anxietyRate) - .nickname(nickname) - .isStartLoveDateUpdated(isStartLoveDateUpdated) + .description(loveTypeCategory == null ? null : loveTypeCategory.getDescription()) .build(); } } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/member/MemberEntity.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/member/MemberEntity.java index a4154877..a61a8fa9 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/member/MemberEntity.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/member/MemberEntity.java @@ -13,6 +13,7 @@ import makeus.cmc.malmo.domain.value.type.EmailForwardingStatus; import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; import makeus.cmc.malmo.domain.value.type.MemberRole; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; import makeus.cmc.malmo.domain.value.type.Provider; import makeus.cmc.malmo.domain.value.type.RelationshipStatus; @@ -74,7 +75,10 @@ public class MemberEntity extends BaseTimeEntity { @Enumerated(value = EnumType.STRING) private RelationshipStatus relationshipStatus; - private String personalityType; + private String mbti; - private String otherPersonalityType; + private String partnerMbti; + + @Enumerated(value = EnumType.STRING) + private PartnerLoveTypeCategory partnerLoveTypeCategory; } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/MemberMapper.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/MemberMapper.java index 30ac893e..f0d09fff 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/MemberMapper.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/MemberMapper.java @@ -34,8 +34,9 @@ public Member toDomain(MemberEntity entity) { entity.getOauthToken(), entity.getCoupleEntityId() != null ? CoupleId.of(entity.getCoupleEntityId().getValue()) : null, entity.getRelationshipStatus(), - entity.getPersonalityType(), - entity.getOtherPersonalityType(), + entity.getMbti(), + entity.getPartnerMbti(), + entity.getPartnerLoveTypeCategory(), entity.getCreatedAt(), entity.getModifiedAt(), entity.getDeletedAt() @@ -65,11 +66,12 @@ public MemberEntity toEntity(Member domain) { .oauthToken(domain.getOauthToken()) .coupleEntityId(domain.getCoupleId() != null ? CoupleEntityId.of(domain.getCoupleId().getValue()) : null) .relationshipStatus(domain.getRelationshipStatus()) - .personalityType(domain.getPersonalityType()) - .otherPersonalityType(domain.getOtherPersonalityType()) + .mbti(domain.getMbti()) + .partnerMbti(domain.getPartnerMbti()) + .partnerLoveTypeCategory(domain.getPartnerLoveTypeCategory()) .createdAt(domain.getCreatedAt()) .modifiedAt(domain.getModifiedAt()) .deletedAt(domain.getDeletedAt()) .build(); } -} \ No newline at end of file +} 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 ca2d630f..f3965462 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 @@ -1,12 +1,10 @@ package makeus.cmc.malmo.adaptor.out.persistence.repository.member; import com.querydsl.core.types.Projections; -import com.querydsl.core.types.dsl.CaseBuilder; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import makeus.cmc.malmo.adaptor.out.persistence.adapter.MemberPersistenceAdapter; -import makeus.cmc.malmo.adaptor.out.persistence.entity.member.QMemberEntity; import makeus.cmc.malmo.adaptor.out.persistence.entity.value.InviteCodeEntityValue; import makeus.cmc.malmo.application.port.out.chat.LoadChatRoomMetadataPort; import makeus.cmc.malmo.domain.value.state.CoupleState; @@ -36,8 +34,9 @@ public Optional findMember memberEntity.nickname, memberEntity.email, memberEntity.relationshipStatus, - memberEntity.personalityType, - memberEntity.otherPersonalityType + memberEntity.mbti, + memberEntity.partnerMbti, + memberEntity.partnerLoveTypeCategory )) .from(memberEntity) .leftJoin(coupleEntity) @@ -51,61 +50,14 @@ public Optional findMember @Override public Optional findPartnerMember(Long memberId) { - QMemberEntity partnerMemberEntity = new QMemberEntity("partnerMemberEntity"); - MemberPersistenceAdapter.PartnerMemberRepositoryDto dto = queryFactory .select(Projections.constructor(MemberPersistenceAdapter.PartnerMemberRepositoryDto.class, - coupleEntity.coupleState.stringValue(), - // 파트너가 실제 멤버면 memberEntity 정보, 아니면 스냅샷 정보 사용 - new CaseBuilder() - .when(partnerMemberEntity.coupleEntityId.value.eq(coupleEntity.id)) - .then(partnerMemberEntity.loveTypeCategory) - .otherwise( - new CaseBuilder() - .when(coupleEntity.firstMemberId.value.eq(memberId)) - .then(coupleEntity.secondMemberSnapshot.loveTypeCategory) - .otherwise(coupleEntity.firstMemberSnapshot.loveTypeCategory) - ), - new CaseBuilder() - .when(partnerMemberEntity.coupleEntityId.value.eq(coupleEntity.id)) - .then(partnerMemberEntity.avoidanceRate) - .otherwise( - new CaseBuilder() - .when(coupleEntity.firstMemberId.value.eq(memberId)) - .then(coupleEntity.secondMemberSnapshot.avoidanceRate) - .otherwise(coupleEntity.firstMemberSnapshot.avoidanceRate) - ), - new CaseBuilder() - .when(partnerMemberEntity.coupleEntityId.value.eq(coupleEntity.id)) - .then(partnerMemberEntity.anxietyRate) - .otherwise( - new CaseBuilder() - .when(coupleEntity.firstMemberId.value.eq(memberId)) - .then(coupleEntity.secondMemberSnapshot.anxietyRate) - .otherwise(coupleEntity.firstMemberSnapshot.anxietyRate) - ), - new CaseBuilder() - .when(partnerMemberEntity.coupleEntityId.value.eq(coupleEntity.id)) - .then(partnerMemberEntity.nickname) - .otherwise( - new CaseBuilder() - .when(coupleEntity.firstMemberId.value.eq(memberId)) - .then(coupleEntity.secondMemberSnapshot.nickname) - .otherwise(coupleEntity.firstMemberSnapshot.nickname) - ), - coupleEntity.isStartLoveDateUpdated + memberEntity.partnerMbti, + memberEntity.partnerLoveTypeCategory )) .from(memberEntity) - .join(coupleEntity).on(memberEntity.coupleEntityId.value.eq(coupleEntity.id)) - .leftJoin(partnerMemberEntity).on( - partnerMemberEntity.id.eq( - new CaseBuilder() - .when(coupleEntity.firstMemberId.value.eq(memberId)) - .then(coupleEntity.secondMemberId.value) - .otherwise(coupleEntity.firstMemberId.value) - ) - ) - .where(memberEntity.id.eq(memberId)) + .where(memberEntity.id.eq(memberId) + .and(memberEntity.partnerMbti.isNotNull())) .fetchOne(); return Optional.ofNullable(dto); @@ -143,32 +95,13 @@ public Optional findInviteCodeByMemberId(Long memberId) { @Override public Optional loadChatRoomMetadata(Long memberId) { - QMemberEntity partnerMemberEntity = new QMemberEntity("partnerMemberEntity"); - LoadChatRoomMetadataPort.ChatRoomMetadataDto dto = queryFactory .select(Projections.constructor( LoadChatRoomMetadataPort.ChatRoomMetadataDto.class, memberEntity.loveTypeCategory, - new CaseBuilder() - .when(partnerMemberEntity.coupleEntityId.value.eq(coupleEntity.id)) - .then(partnerMemberEntity.loveTypeCategory) - .otherwise( - new CaseBuilder() - .when(coupleEntity.firstMemberId.value.eq(memberId)) - .then(coupleEntity.secondMemberSnapshot.loveTypeCategory) - .otherwise(coupleEntity.firstMemberSnapshot.loveTypeCategory) - ) + memberEntity.partnerLoveTypeCategory )) .from(memberEntity) - .leftJoin(coupleEntity).on(memberEntity.coupleEntityId.value.eq(coupleEntity.id)) - .leftJoin(partnerMemberEntity).on( - partnerMemberEntity.id.eq( - new CaseBuilder() - .when(coupleEntity.firstMemberId.value.eq(memberId)) - .then(coupleEntity.secondMemberId.value) - .otherwise(coupleEntity.firstMemberId.value) - ) - ) .where(memberEntity.id.eq(memberId)) .fetchOne(); diff --git a/src/main/java/makeus/cmc/malmo/application/exception/PartnerProfileAlreadyExistsException.java b/src/main/java/makeus/cmc/malmo/application/exception/PartnerProfileAlreadyExistsException.java new file mode 100644 index 00000000..3bbfc820 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/exception/PartnerProfileAlreadyExistsException.java @@ -0,0 +1,7 @@ +package makeus.cmc.malmo.application.exception; + +public class PartnerProfileAlreadyExistsException extends IllegalArgumentException { + public PartnerProfileAlreadyExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/makeus/cmc/malmo/application/exception/PartnerProfileNotFoundException.java b/src/main/java/makeus/cmc/malmo/application/exception/PartnerProfileNotFoundException.java new file mode 100644 index 00000000..e7cb011f --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/exception/PartnerProfileNotFoundException.java @@ -0,0 +1,7 @@ +package makeus.cmc.malmo.application.exception; + +public class PartnerProfileNotFoundException extends IllegalArgumentException { + public PartnerProfileNotFoundException(String message) { + super(message); + } +} 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 80f3090c..0556d7f7 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 @@ -7,11 +7,13 @@ import makeus.cmc.malmo.application.exception.MemberNotFoundException; import makeus.cmc.malmo.application.exception.NotCoupleMemberException; import makeus.cmc.malmo.application.exception.NotValidCoupleCodeException; +import makeus.cmc.malmo.application.exception.PartnerProfileNotFoundException; import makeus.cmc.malmo.application.port.out.member.*; import makeus.cmc.malmo.domain.model.member.Member; import makeus.cmc.malmo.domain.value.id.InviteCodeValue; import makeus.cmc.malmo.domain.value.id.MemberId; import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; import makeus.cmc.malmo.domain.value.type.Provider; import makeus.cmc.malmo.domain.value.type.RelationshipStatus; import org.springframework.stereotype.Component; @@ -49,7 +51,7 @@ public MemberInfoDto getMemberInfoOrThrow(MemberId memberId) { public PartnerMemberDto getPartnerInfoOrThrow(MemberId memberId) { return loadPartnerPort.loadPartnerByMemberId(memberId) - .orElseThrow(MemberNotFoundException::new); + .orElseThrow(() -> new PartnerProfileNotFoundException("등록된 상대 프로필이 없습니다.")); } public Optional getMemberByProviderId(Provider provider, String providerId) { @@ -102,19 +104,17 @@ public static class MemberInfoDto { private int totalCoupleQuestionCount; private RelationshipStatus relationshipStatus; - private String personalityType; - private String otherPersonalityType; + private String mbti; + private String partnerMbti; + private PartnerLoveTypeCategory partnerLoveTypeCategory; } @Data @Builder public static class PartnerMemberDto { - private String memberState; - private LoveTypeCategory loveTypeCategory; - private float avoidanceRate; - private float anxietyRate; - private String nickname; - private Boolean isStartLoveDateUpdated; + private String mbti; + private PartnerLoveTypeCategory loveTypeCategory; + private String description; } } diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/member/CreatePartnerProfileUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/member/CreatePartnerProfileUseCase.java new file mode 100644 index 00000000..bab5ea6d --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/port/in/member/CreatePartnerProfileUseCase.java @@ -0,0 +1,26 @@ +package makeus.cmc.malmo.application.port.in.member; + +import lombok.Builder; +import lombok.Data; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; + +public interface CreatePartnerProfileUseCase { + + PartnerProfileResponseDto createPartnerProfile(CreatePartnerProfileCommand command); + + @Data + @Builder + class CreatePartnerProfileCommand { + private Long memberId; + private String mbti; + private PartnerLoveTypeCategory loveTypeCategory; + } + + @Data + @Builder + class PartnerProfileResponseDto { + private String mbti; + private PartnerLoveTypeCategory loveTypeCategory; + private String description; + } +} diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/member/GetMemberUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/member/GetMemberUseCase.java index 2c70c681..0bb9aabd 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/in/member/GetMemberUseCase.java +++ b/src/main/java/makeus/cmc/malmo/application/port/in/member/GetMemberUseCase.java @@ -4,6 +4,7 @@ import lombok.Data; import makeus.cmc.malmo.domain.value.state.MemberState; import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; import makeus.cmc.malmo.domain.value.type.Provider; import makeus.cmc.malmo.domain.value.type.RelationshipStatus; @@ -37,7 +38,8 @@ class MemberResponseDto { private String email; private RelationshipStatus relationshipStatus; - private String personalityType; - private String otherPersonalityType; + private String mbti; + private String partnerMbti; + private PartnerLoveTypeCategory partnerLoveTypeCategory; } } 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 ae2c7776..8f4d0f69 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 @@ -2,8 +2,7 @@ import lombok.Builder; import lombok.Data; -import makeus.cmc.malmo.domain.value.state.MemberState; -import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; public interface GetPartnerUseCase { @@ -18,11 +17,8 @@ class PartnerInfoCommand { @Data @Builder class PartnerMemberResponseDto { - private MemberState memberState; - private LoveTypeCategory loveTypeCategory; - private float avoidanceRate; - private float anxietyRate; - private String nickname; - private Boolean isStartLoveDateUpdated; + private String mbti; + private PartnerLoveTypeCategory loveTypeCategory; + private String description; } } diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdateMemberUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdateMemberUseCase.java index 93910217..4ff10734 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdateMemberUseCase.java +++ b/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdateMemberUseCase.java @@ -2,6 +2,7 @@ import lombok.Builder; import lombok.Data; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; import makeus.cmc.malmo.domain.value.type.RelationshipStatus; public interface UpdateMemberUseCase { @@ -14,8 +15,8 @@ class UpdateMemberCommand { private Long memberId; private String nickname; private RelationshipStatus relationshipStatus; - private String personalityType; - private String otherPersonalityType; + private String mbti; + private LoveTypeCategory loveTypeCategory; } @Data @@ -23,7 +24,7 @@ class UpdateMemberCommand { class UpdateMemberResponseDto { private String nickname; private RelationshipStatus relationshipStatus; - private String personalityType; - private String otherPersonalityType; + private String mbti; + private LoveTypeCategory loveTypeCategory; } } diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdatePartnerProfileUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdatePartnerProfileUseCase.java new file mode 100644 index 00000000..204b02d3 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdatePartnerProfileUseCase.java @@ -0,0 +1,20 @@ +package makeus.cmc.malmo.application.port.in.member; + +import lombok.Builder; +import lombok.Data; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; + +public interface UpdatePartnerProfileUseCase { + + CreatePartnerProfileUseCase.PartnerProfileResponseDto updatePartnerProfile(UpdatePartnerProfileCommand command); + + @Data + @Builder + class UpdatePartnerProfileCommand { + private Long memberId; + private String mbti; + private boolean mbtiProvided; + private PartnerLoveTypeCategory loveTypeCategory; + private boolean loveTypeCategoryProvided; + } +} diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadChatRoomMetadataPort.java b/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadChatRoomMetadataPort.java index 87dc7dcb..e697e85c 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadChatRoomMetadataPort.java +++ b/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadChatRoomMetadataPort.java @@ -2,6 +2,7 @@ import makeus.cmc.malmo.domain.value.id.MemberId; import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; import java.util.Optional; @@ -9,5 +10,5 @@ public interface LoadChatRoomMetadataPort { Optional loadChatRoomMetadata(MemberId memberId); - record ChatRoomMetadataDto(LoveTypeCategory memberLoveType, LoveTypeCategory partnerLoveType) {} + record ChatRoomMetadataDto(LoveTypeCategory memberLoveType, PartnerLoveTypeCategory partnerLoveType) {} } 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 5947a37d..af9358b3 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 @@ -81,11 +81,11 @@ private String getMetaDataContent(Member member) { String relationshipStatus = member.getRelationshipStatus() != null ? member.getRelationshipStatus().name() : "알 수 없음"; metadataBuilder.append("- 사용자 연애 상태: ").append(relationshipStatus).append("\n"); - String personalityType = member.getPersonalityType() != null ? member.getPersonalityType() : "알 수 없음"; - metadataBuilder.append("- 사용자 MBTI: ").append(personalityType).append("\n"); + String mbti = member.getMbti() != null ? member.getMbti() : "알 수 없음"; + metadataBuilder.append("- 사용자 MBTI: ").append(mbti).append("\n"); - String otherPersonalityType = member.getOtherPersonalityType() != null ? member.getOtherPersonalityType() : "알 수 없음"; - metadataBuilder.append("- 상대방 MBTI: ").append(otherPersonalityType).append("\n"); + String partnerMbti = member.getPartnerMbti() != null ? member.getPartnerMbti() : "알 수 없음"; + metadataBuilder.append("- 상대방 MBTI: ").append(partnerMbti).append("\n"); // String dDayState = memberDomainService.getMemberDDayState(member.getStartLoveDate()); // metadataBuilder.append("- 연애 기간: ").append(dDayState).append("\n"); @@ -94,7 +94,7 @@ private String getMetaDataContent(Member member) { String memberLoveTypeTitle = chatRoomMetadataDto.memberLoveType() != null ? chatRoomMetadataDto.memberLoveType().getTitle() : "알 수 없음"; metadataBuilder.append("- 사용자 애착 유형: ").append(memberLoveTypeTitle).append("\n"); - String partnerLoveType = chatRoomMetadataDto.partnerLoveType() != null ? chatRoomMetadataDto.partnerLoveType().getTitle() : "알 수 없음"; + String partnerLoveType = chatRoomMetadataDto.partnerLoveType() != null ? chatRoomMetadataDto.partnerLoveType().getDescription() : "알 수 없음"; metadataBuilder.append("- 애인 애착 유형: ").append(partnerLoveType).append("\n"); metadataBuilder.append(memberMemoryList); 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 b0ad5ca8..bc5cf147 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 @@ -10,14 +10,19 @@ import makeus.cmc.malmo.application.helper.member.MemberQueryHelper; import makeus.cmc.malmo.application.helper.member.OauthTokenHelper; import makeus.cmc.malmo.application.helper.notification.MemberNotificationCommandHelper; +import makeus.cmc.malmo.application.port.in.member.CreatePartnerProfileUseCase; import makeus.cmc.malmo.application.port.in.member.DeleteMemberUseCase; +import makeus.cmc.malmo.application.port.in.member.UpdatePartnerProfileUseCase; import makeus.cmc.malmo.application.port.in.member.UpdateMemberUseCase; import makeus.cmc.malmo.application.port.in.member.UpdateStartLoveDateUseCase; +import makeus.cmc.malmo.application.exception.PartnerProfileAlreadyExistsException; +import makeus.cmc.malmo.application.exception.PartnerProfileNotFoundException; import makeus.cmc.malmo.application.port.out.sse.SendSseEventPort; import makeus.cmc.malmo.application.port.out.sse.ValidateSsePort; import makeus.cmc.malmo.domain.model.couple.Couple; import makeus.cmc.malmo.domain.model.member.Member; import makeus.cmc.malmo.domain.value.id.MemberId; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; import makeus.cmc.malmo.domain.value.type.Provider; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -28,7 +33,12 @@ @Service @RequiredArgsConstructor -public class MemberCommandService implements UpdateMemberUseCase, UpdateStartLoveDateUseCase, DeleteMemberUseCase { +public class MemberCommandService implements + UpdateMemberUseCase, + CreatePartnerProfileUseCase, + UpdatePartnerProfileUseCase, + UpdateStartLoveDateUseCase, + DeleteMemberUseCase { private final CoupleQueryHelper coupleQueryHelper; private final CoupleCommandHelper coupleCommandHelper; @@ -51,8 +61,8 @@ public UpdateMemberResponseDto updateMember(UpdateMemberCommand command) { member.updateMemberProfile( command.getNickname(), command.getRelationshipStatus(), - command.getPersonalityType(), - command.getOtherPersonalityType() + command.getMbti(), + command.getLoveTypeCategory() ); Member savedMember = memberCommandHelper.saveMember(member); @@ -60,11 +70,48 @@ public UpdateMemberResponseDto updateMember(UpdateMemberCommand command) { return UpdateMemberResponseDto.builder() .nickname(savedMember.getNickname()) .relationshipStatus(savedMember.getRelationshipStatus()) - .personalityType(savedMember.getPersonalityType()) - .otherPersonalityType(savedMember.getOtherPersonalityType()) + .mbti(savedMember.getMbti()) + .loveTypeCategory(savedMember.getLoveTypeCategory()) .build(); } + @Override + @CheckValidMember + @Transactional + public PartnerProfileResponseDto createPartnerProfile(CreatePartnerProfileCommand command) { + Member member = memberQueryHelper.getMemberByIdOrThrow(MemberId.of(command.getMemberId())); + if (member.hasPartnerProfile()) { + throw new PartnerProfileAlreadyExistsException("이미 상대 프로필이 등록되어 있습니다."); + } + + PartnerLoveTypeCategory partnerLoveTypeCategory = resolvePartnerLoveTypeCategory(command.getLoveTypeCategory()); + member.createPartnerProfile(command.getMbti(), partnerLoveTypeCategory); + Member savedMember = memberCommandHelper.saveMember(member); + + return toPartnerProfileResponse(savedMember); + } + + @Override + @CheckValidMember + @Transactional + public PartnerProfileResponseDto updatePartnerProfile(UpdatePartnerProfileCommand command) { + Member member = memberQueryHelper.getMemberByIdOrThrow(MemberId.of(command.getMemberId())); + if (!member.hasPartnerProfile()) { + throw new PartnerProfileNotFoundException("등록된 상대 프로필이 없습니다."); + } + + String partnerMbti = command.isMbtiProvided() ? command.getMbti() : null; + PartnerLoveTypeCategory partnerLoveTypeCategory = null; + if (command.isLoveTypeCategoryProvided()) { + partnerLoveTypeCategory = resolvePartnerLoveTypeCategory(command.getLoveTypeCategory()); + } + + member.updatePartnerProfile(partnerMbti, partnerLoveTypeCategory); + Member savedMember = memberCommandHelper.saveMember(member); + + return toPartnerProfileResponse(savedMember); + } + @Override @CheckCoupleMember @Transactional @@ -131,4 +178,18 @@ public void coupleUnlink(Member member, Couple couple) { memberNotificationCommandHelper.createAndSaveCoupleDisconnectedNotification(partnerId); } } + + private PartnerProfileResponseDto toPartnerProfileResponse(Member savedMember) { + return PartnerProfileResponseDto.builder() + .mbti(savedMember.getPartnerMbti()) + .loveTypeCategory(savedMember.getPartnerLoveTypeCategory()) + .description(savedMember.getPartnerLoveTypeCategory() == null + ? null + : savedMember.getPartnerLoveTypeCategory().getDescription()) + .build(); + } + + private PartnerLoveTypeCategory resolvePartnerLoveTypeCategory(PartnerLoveTypeCategory loveTypeCategory) { + return loveTypeCategory == null ? PartnerLoveTypeCategory.UNKNOWN : loveTypeCategory; + } } 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 b59e5765..f187ffa1 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 @@ -1,7 +1,6 @@ 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.member.MemberQueryHelper; import makeus.cmc.malmo.application.port.in.member.GetMemberUseCase; @@ -33,23 +32,21 @@ public MemberResponseDto getMemberInfo(MemberInfoCommand command) { .nickname(member.getNickname()) .email(member.getEmail()) .relationshipStatus(member.getRelationshipStatus()) - .personalityType(member.getPersonalityType()) - .otherPersonalityType(member.getOtherPersonalityType()) + .mbti(member.getMbti()) + .partnerMbti(member.getPartnerMbti()) + .partnerLoveTypeCategory(member.getPartnerLoveTypeCategory()) .build(); } @Override - @CheckCoupleMember + @CheckValidMember public PartnerMemberResponseDto getPartnerInfo(PartnerInfoCommand command) { MemberQueryHelper.PartnerMemberDto partner = memberQueryHelper.getPartnerInfoOrThrow(MemberId.of(command.getUserId())); return PartnerMemberResponseDto.builder() - .memberState(MemberState.valueOf(partner.getMemberState())) + .mbti(partner.getMbti()) .loveTypeCategory(partner.getLoveTypeCategory()) - .avoidanceRate(partner.getAvoidanceRate()) - .anxietyRate(partner.getAnxietyRate()) - .nickname(partner.getNickname()) - .isStartLoveDateUpdated(partner.getIsStartLoveDateUpdated()) + .description(partner.getDescription()) .build(); } } 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 461aaa9b..6e26d97a 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 @@ -9,6 +9,7 @@ import makeus.cmc.malmo.domain.value.type.EmailForwardingStatus; import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; import makeus.cmc.malmo.domain.value.type.MemberRole; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; import makeus.cmc.malmo.domain.value.type.Provider; import makeus.cmc.malmo.domain.value.type.RelationshipStatus; @@ -45,8 +46,9 @@ public class Member { private CoupleId coupleId; private RelationshipStatus relationshipStatus; - private String personalityType; - private String otherPersonalityType; + private String mbti; + private String partnerMbti; + private PartnerLoveTypeCategory partnerLoveTypeCategory; // BaseTimeEntity fields private LocalDateTime createdAt; @@ -87,8 +89,9 @@ public static Member from( String oauthToken, CoupleId coupleId, RelationshipStatus relationshipStatus, - String personalityType, - String otherPersonalityType, + String mbti, + String partnerMbti, + PartnerLoveTypeCategory partnerLoveTypeCategory, LocalDateTime createdAt, LocalDateTime modifiedAt, LocalDateTime deletedAt @@ -113,8 +116,9 @@ public static Member from( .oauthToken(oauthToken) .coupleId(coupleId) .relationshipStatus(relationshipStatus) - .personalityType(personalityType) - .otherPersonalityType(otherPersonalityType) + .mbti(normalizeMbti(mbti)) + .partnerMbti(normalizeMbti(partnerMbti)) + .partnerLoveTypeCategory(partnerLoveTypeCategory) .createdAt(createdAt) .modifiedAt(modifiedAt) .deletedAt(deletedAt) @@ -139,18 +143,36 @@ public void signUp(String nickname, RelationshipStatus relationshipStatus) { this.memberState = MemberState.ALIVE; } - public void updateMemberProfile(String nickname, RelationshipStatus relationshipStatus, String personalityType, String otherPersonalityType) { + public void updateMemberProfile(String nickname, RelationshipStatus relationshipStatus, String mbti, LoveTypeCategory loveTypeCategory) { if (nickname != null) { this.nickname = nickname; } if (relationshipStatus != null) { this.relationshipStatus = relationshipStatus; } - if (personalityType != null) { - this.personalityType = personalityType; + if (mbti != null) { + this.mbti = normalizeMbti(mbti); } - if (otherPersonalityType != null) { - this.otherPersonalityType = otherPersonalityType; + if (loveTypeCategory != null) { + this.loveTypeCategory = loveTypeCategory; + } + } + + public boolean hasPartnerProfile() { + return this.partnerMbti != null; + } + + public void createPartnerProfile(String partnerMbti, PartnerLoveTypeCategory partnerLoveTypeCategory) { + this.partnerMbti = normalizeMbti(partnerMbti); + this.partnerLoveTypeCategory = partnerLoveTypeCategory; + } + + public void updatePartnerProfile(String partnerMbti, PartnerLoveTypeCategory partnerLoveTypeCategory) { + if (partnerMbti != null) { + this.partnerMbti = normalizeMbti(partnerMbti); + } + if (partnerLoveTypeCategory != null) { + this.partnerLoveTypeCategory = partnerLoveTypeCategory; } } @@ -211,4 +233,8 @@ public void linkCouple(CoupleId coupleId) { public void unlinkCouple() { this.coupleId = null; } + + private static String normalizeMbti(String mbti) { + return mbti == null ? null : mbti.toUpperCase(); + } } diff --git a/src/main/java/makeus/cmc/malmo/domain/value/type/PartnerLoveTypeCategory.java b/src/main/java/makeus/cmc/malmo/domain/value/type/PartnerLoveTypeCategory.java new file mode 100644 index 00000000..91bb8f6f --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/domain/value/type/PartnerLoveTypeCategory.java @@ -0,0 +1,24 @@ +package makeus.cmc.malmo.domain.value.type; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum PartnerLoveTypeCategory { + STABLE_TYPE("안정형"), + ANXIETY_TYPE("불안형"), + AVOIDANCE_TYPE("회피형"), + CONFUSION_TYPE("혼란형"), + UNKNOWN("모르겠어요"); + + private final String description; + + public static PartnerLoveTypeCategory fromLoveTypeCategory(LoveTypeCategory loveTypeCategory) { + if (loveTypeCategory == null) { + return UNKNOWN; + } + + return valueOf(loveTypeCategory.name()); + } +} 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 80bb2edb..c9ced722 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/CoupleIntegrationTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/CoupleIntegrationTest.java @@ -519,9 +519,9 @@ class CoupleUnLinkFeature { mockMvc.perform(get("/members/partner") .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("message").value(NOT_COUPLE_MEMBER.getMessage())) - .andExpect(jsonPath("code").value(NOT_COUPLE_MEMBER.getCode())); + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("message").value(NO_SUCH_PARTNER_PROFILE.getMessage())) + .andExpect(jsonPath("code").value(NO_SUCH_PARTNER_PROFILE.getCode())); } @Test @@ -538,4 +538,4 @@ class CoupleUnLinkFeature { } -} \ No newline at end of file +} 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 8961b611..b94c7320 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java @@ -23,7 +23,9 @@ 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.PartnerLoveTypeCategory; import makeus.cmc.malmo.domain.value.type.Provider; +import makeus.cmc.malmo.domain.value.type.RelationshipStatus; 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; @@ -824,18 +826,16 @@ public static class MemberResponseDto { String email; String relationshipStatus; - String personalityType; - String otherPersonalityType; + String mbti; + String partnerMbti; + String partnerLoveTypeCategory; } @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) public static class PartnerResponseDto { - private MemberState memberState; - private LoveTypeCategory loveTypeCategory; - private float avoidanceRate; - private float anxietyRate; - private String nickname; - private Boolean isStartLoveDateUpdated; + private String mbti; + private String loveTypeCategory; + private String description; } void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, LocalDate startLoveDate, int coupleQuestionCount, int totalChatRoomCount) { @@ -857,6 +857,10 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc Assertions.assertThat(memberResponse.anxietyRate).isEqualTo(member.getAnxietyRate()); Assertions.assertThat(memberResponse.nickname).isEqualTo(member.getNickname()); Assertions.assertThat(memberResponse.email).isEqualTo(member.getEmail()); + Assertions.assertThat(memberResponse.mbti).isEqualTo(member.getMbti()); + Assertions.assertThat(memberResponse.partnerMbti).isEqualTo(member.getPartnerMbti()); + Assertions.assertThat(memberResponse.partnerLoveTypeCategory) + .isEqualTo(member.getPartnerLoveTypeCategory() == null ? null : member.getPartnerLoveTypeCategory().name()); } @Test @@ -983,18 +987,22 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc } @Test - @DisplayName("파트너 멤버 정보 조회 성공") + @DisplayName("상대 프로필 등록 및 조회 성공") void 파트너_멤버_정보_조회_성공() throws Exception { // given - MemberEntity partner = createAndSavePartner(); + Map requestDto = Map.of( + "mbti", "enfp", + "loveTypeCategory", "CONFUSION_TYPE" + ); - mockMvc.perform(post("/couples") + mockMvc.perform(post("/members/partners") .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString( - CoupleRequestDtoFactory.createCoupleLinkRequestDto(partner.getInviteCodeEntityValue().getValue()) - ))) - .andExpect(status().isOk()); + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.mbti").value("ENFP")) + .andExpect(jsonPath("$.data.loveTypeCategory").value("CONFUSION_TYPE")) + .andExpect(jsonPath("$.data.description").value("혼란형")); // when MvcResult mvcResult = mockMvc.perform(get("/members/partner") @@ -1010,41 +1018,32 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc new TypeReference<>() {} ); - // 파트너 멤버 정보가 정상적으로 조회되었는지 검증 + // 상대 프로필이 정상적으로 조회되는지 검증 PartnerResponseDto partnerDto = responseDto.data; - Assertions.assertThat(partnerDto.memberState).isEqualTo(partner.getMemberState()); - Assertions.assertThat(partnerDto.loveTypeCategory).isEqualTo(partner.getLoveTypeCategory()); - 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(); + Assertions.assertThat(partnerDto.mbti).isEqualTo("ENFP"); + Assertions.assertThat(partnerDto.loveTypeCategory).isEqualTo("CONFUSION_TYPE"); + Assertions.assertThat(partnerDto.description).isEqualTo("혼란형"); } @Test - @DisplayName("디데이 변경 후 파트너 정보 조회 시 isStartLoveDateUpdated가 true인지 확인") + @DisplayName("상대 프로필 수정 성공") void 디데이_변경_후_파트너_정보_조회_시_isStartLoveDateUpdated_확인() throws Exception { // given - MemberEntity partner = createAndSavePartner(); - - // 커플 연결 - mockMvc.perform(post("/couples") + mockMvc.perform(post("/members/partners") .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString( - CoupleRequestDtoFactory.createCoupleLinkRequestDto(partner.getInviteCodeEntityValue().getValue()) - ))) + .content(objectMapper.writeValueAsString(Map.of("mbti", "enfp")))) .andExpect(status().isOk()); - // 디데이 변경 - LocalDate newDday = LocalDate.of(2025, 1, 1); - mockMvc.perform(patch("/members/start-love-date") + mockMvc.perform(patch("/members/partners") .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString( - MemberRequestDtoFactory.createUpdateStartLoveDateRequestDto(newDday) - ))) - .andExpect(status().isOk()); + .content(""" + {"loveTypeCategory":null} + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.loveTypeCategory").value("UNKNOWN")) + .andExpect(jsonPath("$.data.description").value("모르겠어요")); // when MvcResult mvcResult = mockMvc.perform(get("/members/partner") @@ -1060,50 +1059,63 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc new TypeReference<>() {} ); - // 디데이 변경 후 isStartLoveDateUpdated가 true인지 확인 PartnerResponseDto partnerDto = responseDto.data; - Assertions.assertThat(partnerDto.isStartLoveDateUpdated).isTrue(); + Assertions.assertThat(partnerDto.mbti).isEqualTo("ENFP"); + Assertions.assertThat(partnerDto.loveTypeCategory).isEqualTo("UNKNOWN"); + Assertions.assertThat(partnerDto.description).isEqualTo("모르겠어요"); } @Test - @DisplayName("파트너 멤버 정보 조회 실패 - 커플이 아닌 경우") + @DisplayName("상대 프로필 조회 실패 - 미등록 상태") void 파트너_멤버_정보_조회_실패_커플이_아닌_경우() throws Exception { // when & then mockMvc.perform(get("/members/partner") .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("message").value(NOT_COUPLE_MEMBER.getMessage())) - .andExpect(jsonPath("code").value(NOT_COUPLE_MEMBER.getCode())); + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("message").value(NO_SUCH_PARTNER_PROFILE.getMessage())) + .andExpect(jsonPath("code").value(NO_SUCH_PARTNER_PROFILE.getCode())); } @Test - @DisplayName("파트너 멤버 정보 조회 실패 - 탈퇴한 멤버인 경우") + @DisplayName("상대 프로필 중복 등록 실패") void 파트너_멤버_정보_조회_실패_탈퇴한_멤버인_경우() throws Exception { // given - MemberEntity partner = createAndSavePartner(); - - mockMvc.perform(post("/couples") + mockMvc.perform(post("/members/partners") .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString( - CoupleRequestDtoFactory.createCoupleLinkRequestDto(partner.getInviteCodeEntityValue().getValue()) - ))) + .content(objectMapper.writeValueAsString(Map.of("mbti", "ENFP")))) .andExpect(status().isOk()); - // 탈퇴 처리 - mockMvc.perform(delete("/members") + mockMvc.perform(post("/members/partners") .header("Authorization", "Bearer " + accessToken) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(Map.of("mbti", "INTJ")))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("message").value(PARTNER_PROFILE_ALREADY_EXISTS.getMessage())) + .andExpect(jsonPath("code").value(PARTNER_PROFILE_ALREADY_EXISTS.getCode())); + } - // when & then - mockMvc.perform(get("/members/partner") + @Test + @DisplayName("멤버 정보 수정 시 MBTI와 애착 유형 반영") + void 멤버_정보_수정_시_MBTI와_애착유형_반영() throws Exception { + mockMvc.perform(patch("/members") .header("Authorization", "Bearer " + accessToken) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("message").value(NOT_COUPLE_MEMBER.getMessage())) - .andExpect(jsonPath("code").value(NOT_COUPLE_MEMBER.getCode())); + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"relationshipStatus":"IN_RELATIONSHIP","mbti":"intj","loveTypeCategory":"STABLE_TYPE"} + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.relationshipStatus").value(RelationshipStatus.IN_RELATIONSHIP.name())) + .andExpect(jsonPath("$.data.mbti").value("INTJ")) + .andExpect(jsonPath("$.data.loveTypeCategory").value(LoveTypeCategory.STABLE_TYPE.name())); + + em.flush(); + em.clear(); + + MemberEntity updatedMember = em.find(MemberEntity.class, member.getId()); + Assertions.assertThat(updatedMember.getMbti()).isEqualTo("INTJ"); + Assertions.assertThat(updatedMember.getLoveTypeCategory()).isEqualTo(LoveTypeCategory.STABLE_TYPE); } @Test @@ -1560,4 +1572,4 @@ private ChatRoomEntity createAndSaveChatRoom() { return chatRoom; } -} \ No newline at end of file +} diff --git a/src/test/java/makeus/cmc/malmo/mapper/MemberMapperTest.java b/src/test/java/makeus/cmc/malmo/mapper/MemberMapperTest.java index 07c32db3..ac9fee3f 100644 --- a/src/test/java/makeus/cmc/malmo/mapper/MemberMapperTest.java +++ b/src/test/java/makeus/cmc/malmo/mapper/MemberMapperTest.java @@ -10,6 +10,7 @@ import makeus.cmc.malmo.domain.value.state.MemberState; import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; import makeus.cmc.malmo.domain.value.type.MemberRole; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; import makeus.cmc.malmo.domain.value.type.Provider; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -84,6 +85,9 @@ private void assertMember(Member domain, MemberEntity entity) { assertThat(domain.getStartLoveDate()).isEqualTo(entity.getStartLoveDate()); assertThat(domain.getOauthToken()).isEqualTo(entity.getOauthToken()); assertThat(domain.getCoupleId().getValue()).isEqualTo(entity.getCoupleEntityId().getValue()); + assertThat(domain.getMbti()).isEqualTo(entity.getMbti()); + assertThat(domain.getPartnerMbti()).isEqualTo(entity.getPartnerMbti()); + assertThat(domain.getPartnerLoveTypeCategory()).isEqualTo(entity.getPartnerLoveTypeCategory()); assertThat(domain.getCreatedAt()).isEqualTo(entity.getCreatedAt()); assertThat(domain.getModifiedAt()).isEqualTo(entity.getModifiedAt()); assertThat(domain.getDeletedAt()).isEqualTo(entity.getDeletedAt()); @@ -107,6 +111,9 @@ private void assertMemberEntity(MemberEntity entity, Member domain) { assertThat(entity.getStartLoveDate()).isEqualTo(domain.getStartLoveDate()); assertThat(entity.getOauthToken()).isEqualTo(domain.getOauthToken()); assertThat(entity.getCoupleEntityId().getValue()).isEqualTo(domain.getCoupleId().getValue()); + assertThat(entity.getMbti()).isEqualTo(domain.getMbti()); + assertThat(entity.getPartnerMbti()).isEqualTo(domain.getPartnerMbti()); + assertThat(entity.getPartnerLoveTypeCategory()).isEqualTo(domain.getPartnerLoveTypeCategory()); assertThat(entity.getCreatedAt()).isEqualTo(domain.getCreatedAt()); assertThat(entity.getModifiedAt()).isEqualTo(domain.getModifiedAt()); assertThat(entity.getDeletedAt()).isEqualTo(domain.getDeletedAt()); @@ -132,6 +139,9 @@ private MemberEntity createCompleteEntity() { .startLoveDate(LocalDate.now()) .oauthToken("oauth_token") .coupleEntityId(CoupleEntityId.of(100L)) + .mbti("INTJ") + .partnerMbti("ENFP") + .partnerLoveTypeCategory(PartnerLoveTypeCategory.UNKNOWN) .createdAt(now) .modifiedAt(now) .deletedAt(null) @@ -160,8 +170,9 @@ private Member createCompleteMember() { "oauth_token", CoupleId.of(100L), null, // relationshipStatus - null, // personalityType - null, // otherPersonalityType + "INTJ", + "ENFP", + PartnerLoveTypeCategory.UNKNOWN, now, now, null diff --git a/src/test/java/makeus/cmc/malmo/service/AppleNotificationServiceTest.java b/src/test/java/makeus/cmc/malmo/service/AppleNotificationServiceTest.java index aeaac1a3..f9b2dd2f 100644 --- a/src/test/java/makeus/cmc/malmo/service/AppleNotificationServiceTest.java +++ b/src/test/java/makeus/cmc/malmo/service/AppleNotificationServiceTest.java @@ -12,6 +12,7 @@ import makeus.cmc.malmo.domain.value.state.MemberState; import makeus.cmc.malmo.domain.value.type.EmailForwardingStatus; import makeus.cmc.malmo.domain.value.type.MemberRole; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; import makeus.cmc.malmo.domain.value.type.Provider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -90,8 +91,9 @@ private Member createTestMember(String providerId, String oauthToken) { oauthToken, null, null, // relationshipStatus - null, // personalityType - null, // otherPersonalityType + null, // mbti + null, // partnerMbti + null, // partnerLoveTypeCategory null, null, null @@ -347,4 +349,3 @@ class TokenInvalidationTest { } } } - From 705f54b5718c691f0d64934d93bb8bb3712cea8a Mon Sep 17 00:00:00 2001 From: Choi wontak <76509639+RoundTable02@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:14:07 +0900 Subject: [PATCH 03/15] =?UTF-8?q?feat:=20=EC=95=A0=EC=B0=A9=EC=9C=A0?= =?UTF-8?q?=ED=98=95=20mbti=20=EA=B2=B0=EA=B3=BC=20=EC=9D=91=EB=8B=B5=20AP?= =?UTF-8?q?I=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/API-CHANGES-LOVETYPE-MBTI-RESULT.md | 198 ++++++++++++++++++ .../malmo/adaptor/in/exception/ErrorCode.java | 1 + .../in/exception/GlobalExceptionHandler.java | 15 +- .../in/web/controller/LoveTypeController.java | 61 ++++++ .../adaptor/in/web/docs/SwaggerResponses.java | 62 ++++++ ...LoveTypeMbtiFeaturePersistenceAdapter.java | 25 +++ .../entity/LoveTypeMbtiFeatureEntity.java | 152 ++++++++++++++ .../value/LoveTypeMbtiFeatureEntityId.java | 16 ++ .../mapper/LoveTypeMbtiFeatureMapper.java | 59 ++++++ .../LoveTypeMbtiFeatureRepository.java | 12 ++ .../LoveTypeMbtiFeatureNotFoundException.java | 4 + .../port/in/GetLoveTypeMbtiResultUseCase.java | 49 +++++ .../port/out/LoadLoveTypeMbtiFeaturePort.java | 10 + .../service/LoveTypeMbtiFeatureService.java | 94 +++++++++ .../model/love_type/LoveTypeMbtiFeature.java | 144 +++++++++++++ .../LoveTypeQuestionTest.java | 115 ++++++++++ 16 files changed, 1016 insertions(+), 1 deletion(-) create mode 100644 docs/API-CHANGES-LOVETYPE-MBTI-RESULT.md create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiFeaturePersistenceAdapter.java create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypeMbtiFeatureEntity.java create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypeMbtiFeatureEntityId.java create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypeMbtiFeatureMapper.java create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiFeatureRepository.java create mode 100644 src/main/java/makeus/cmc/malmo/application/exception/LoveTypeMbtiFeatureNotFoundException.java create mode 100644 src/main/java/makeus/cmc/malmo/application/port/in/GetLoveTypeMbtiResultUseCase.java create mode 100644 src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiFeaturePort.java create mode 100644 src/main/java/makeus/cmc/malmo/application/service/LoveTypeMbtiFeatureService.java create mode 100644 src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypeMbtiFeature.java diff --git a/docs/API-CHANGES-LOVETYPE-MBTI-RESULT.md b/docs/API-CHANGES-LOVETYPE-MBTI-RESULT.md new file mode 100644 index 00000000..ebccaca2 --- /dev/null +++ b/docs/API-CHANGES-LOVETYPE-MBTI-RESULT.md @@ -0,0 +1,198 @@ +## 신규 API 스펙 + +### `GET /love-types/result` - MBTI + 애착유형 상세 결과 조회 + +`mbti`와 `lovetype` 조합으로 MBTI별 애착유형 상세 결과를 조회합니다. + +기존 API와의 차이: +- `POST /love-types/result`: 검사 답변을 제출하고 임시 결과를 생성 +- `GET /love-types/result/{loveTypeId}`: 생성된 임시 검사 결과를 조회 +- `GET /love-types/result`: `(mbti, lovetype)` 조합에 대응하는 고정 상세 콘텐츠를 조회 + +--- + +## Request + +### Query Parameters + +| name | type | required | description | +| --- | --- | --- | --- | +| `mbti` | String | Y | 영문 4자리 MBTI. 대소문자 무관, 내부적으로 대문자로 정규화 | +| `lovetype` | String | Y | `STABLE_TYPE`, `ANXIETY_TYPE`, `AVOIDANCE_TYPE`, `CONFUSION_TYPE` 중 하나. 대소문자 무관 | + +### Example + +```http +GET /love-types/result?mbti=enfp&lovetype=stable_type +``` + +--- + +## Response + +```ts +{ + mbti: string, + loveTypeCategory: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE', + summary: string, + keywords: string[], + strengths: Array<{ + title: string | null, + description: string | null + }>, + weaknesses: Array<{ + title: string | null, + description: string | null + }>, + patterns: Array<{ + title: string | null, + description: string | null + }>, + loveTypeFeatures: Array<{ + title: string | null, + description: string | null + }>, + datingGuides: string[], + bestMatches: Array<{ + mbti: string | null, + description: string | null + }>, + worstMatches: Array<{ + mbti: string | null, + description: string | null + }> +} +``` + +### Example Response + +```json +{ + "mbti": "ENFP", + "loveTypeCategory": "STABLE_TYPE", + "summary": "풍부한 상상력과 사랑으로, 함께하는 일상을 즐겁게 만들어 가는 유형", + "keywords": ["열정적", "자유로움", "생기발랄"], + "strengths": [ + { + "title": "Ne", + "description": "흩어진 정보 속에서 하나의 핵심 맥락과 미래를 읽어내요" + }, + { + "title": "Fi", + "description": "나의 진솔한 감정을 공유하는 것을 최고의 가치로 여겨요" + } + ], + "weaknesses": [ + { + "title": "Si", + "description": "반복되는 루틴에 답답함을 느껴요" + } + ], + "patterns": [ + { + "title": "나다운 기준을 지켜요", + "description": "여러 가능성 속에서 무엇이 나에게 의미 있는지 먼저 생각해요" + } + ], + "loveTypeFeatures": [ + { + "title": "미래의 가능성을 자주 상상해요", + "description": "연인과 함께할 미래의 가능성을 상상하며 창의적인 질문으로 관계를 만들어요" + } + ], + "datingGuides": ["감정을 정리해 표현해요"], + "bestMatches": [ + { + "mbti": "INFJ", + "description": "속마음을 깊이 이해해주며 안정적인 감정을 공유하는 궁합" + } + ], + "worstMatches": [ + { + "mbti": "ISTP", + "description": "자유로운 감정선과 솔직한 피드백이 부딪히는 궁합" + } + ] +} +``` + +--- + +## DB 매핑 기준 + +조회 대상 테이블은 `love_type_mbti_feature`입니다. + +테이블 컬럼은 `docs/mbti-lovetype-feature.md` 헤더를 기준으로 그대로 맞춥니다. + +| column | description | +| --- | --- | +| `lovetype` | 애착유형 enum 문자열 | +| `mbti` | MBTI 4자리 문자열 | +| `summary` | 요약 문구 | +| `keyword1` ~ `keyword3` | 키워드 | +| `strength1` ~ `strength3` | 강점 제목 | +| `strength_desc1` ~ `strength_desc3` | 강점 설명 | +| `weakness` | 약점 제목 | +| `weakness_desc` | 약점 설명 | +| `pattern_title1` ~ `pattern_title4` | 관계 패턴 제목 | +| `pattern1` ~ `pattern4` | 관계 패턴 설명 | +| `lovetype_feature_title1` ~ `lovetype_feature_title4` | 애착유형 특징 제목 | +| `lovetype_feature1` ~ `lovetype_feature4` | 애착유형 특징 설명 | +| `dating_guide1` ~ `dating_guide3` | 연애 가이드 | +| `best_mbti1` ~ `best_mbti2` | 잘 맞는 MBTI | +| `best_desc1` ~ `best_desc2` | 잘 맞는 MBTI 설명 | +| `worst_mbti1` ~ `worst_mbti2` | 부딪히기 쉬운 MBTI | +| `worst_desc1` ~ `worst_desc2` | 부딪히기 쉬운 MBTI 설명 | + +주의: +- 애플리케이션은 별도 audit 컬럼 없이 위 문서 컬럼만 기준으로 조회합니다. +- `(mbti, lovetype)` 조합은 유니크하다고 가정합니다. + +--- + +## 응답 가공 규칙 + +문서의 번호형 컬럼은 응답에서 묶어서 반환합니다. + +- `keyword1` ~ `keyword3` -> `keywords[]` +- `strengthN + strength_descN` -> `strengths[]` +- `weakness + weakness_desc` -> `weaknesses[]` +- `pattern_titleN + patternN` -> `patterns[]` +- `lovetype_feature_titleN + lovetype_featureN` -> `loveTypeFeatures[]` +- `dating_guideN` -> `datingGuides[]` +- `best_mbtiN + best_descN` -> `bestMatches[]` +- `worst_mbtiN + worst_descN` -> `worstMatches[]` + +빈 문자열 또는 null은 응답 배열에서 제외합니다. + +예: +- `strength2=""`, `strength_desc2=""` 이면 `strengths` 배열에 두 번째 항목은 포함되지 않습니다. +- `best_mbti2`만 비어 있어도 `bestMatches` 두 번째 항목은 제외됩니다. + +--- + +## 에러 응답 + +### 잘못된 요청 +- `mbti`가 영문 4자리가 아닌 경우 +- `lovetype`이 허용 enum 값이 아닌 경우 +- 필수 query parameter가 누락된 경우 + +```json +{ + "success": false, + "message": "잘못된 요청입니다.", + "code": 40000 +} +``` + +### 데이터 없음 +- `mbti + lovetype` 조합에 해당하는 row가 없는 경우 + +```json +{ + "success": false, + "message": "해당 MBTI와 애착 유형 결과가 존재하지 않습니다.", + "code": 40019 +} +``` diff --git a/src/main/java/makeus/cmc/malmo/adaptor/in/exception/ErrorCode.java b/src/main/java/makeus/cmc/malmo/adaptor/in/exception/ErrorCode.java index 0121f8f8..c11e6479 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/exception/ErrorCode.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/in/exception/ErrorCode.java @@ -28,6 +28,7 @@ public enum ErrorCode { NO_SUCH_MESSAGE(HttpStatus.BAD_REQUEST, 40016, "메시지가 존재하지 않습니다."), PARTNER_PROFILE_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, 40017, "이미 상대 프로필이 등록되어 있습니다."), NO_SUCH_PARTNER_PROFILE(HttpStatus.BAD_REQUEST, 40018, "등록된 상대 프로필이 존재하지 않습니다."), + NO_SUCH_LOVE_TYPE_MBTI_RESULT(HttpStatus.BAD_REQUEST, 40019, "해당 MBTI와 애착 유형 결과가 존재하지 않습니다."), // 401 Unauthorized UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 40100, "인증되지 않은 사용자입니다."), 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 80f7e11a..c0ccbca8 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 @@ -1,6 +1,7 @@ package makeus.cmc.malmo.adaptor.in.exception; import io.sentry.Sentry; +import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import makeus.cmc.malmo.adaptor.out.exception.OidcIdTokenException; import makeus.cmc.malmo.adaptor.out.exception.RestApiException; @@ -9,6 +10,7 @@ import org.hibernate.TypeMismatchException; import org.springframework.http.ResponseEntity; import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -180,12 +182,23 @@ public ResponseEntity handlePartnerProfileNotFoundException(Partn return ErrorResponse.of(ErrorCode.NO_SUCH_PARTNER_PROFILE); } + @ExceptionHandler({LoveTypeMbtiFeatureNotFoundException.class}) + public ResponseEntity handleLoveTypeMbtiFeatureNotFoundException(LoveTypeMbtiFeatureNotFoundException e) { + log.warn("[GlobalExceptionHandler: handleLoveTypeMbtiFeatureNotFoundException 호출] {}", e.getMessage()); + return ErrorResponse.of(ErrorCode.NO_SUCH_LOVE_TYPE_MBTI_RESULT); + } + /** * ---------- 공통 예외 처리 핸들러 ---------- */ - @ExceptionHandler({NoHandlerFoundException.class, TypeMismatchException.class}) + @ExceptionHandler({ + NoHandlerFoundException.class, + TypeMismatchException.class, + MissingServletRequestParameterException.class, + ConstraintViolationException.class + }) public ResponseEntity handleBadRequestException(Exception e) { log.warn("[CommonExceptionHandler: handleBadRequestException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.BAD_REQUEST); diff --git a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/LoveTypeController.java b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/LoveTypeController.java index 3a69c68a..f5724302 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/LoveTypeController.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/LoveTypeController.java @@ -10,6 +10,7 @@ import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -18,14 +19,20 @@ import makeus.cmc.malmo.adaptor.in.web.dto.BaseListResponse; import makeus.cmc.malmo.adaptor.in.web.dto.BaseResponse; import makeus.cmc.malmo.application.port.in.CalculateQuestionResultUseCase; +import makeus.cmc.malmo.application.port.in.GetLoveTypeMbtiResultUseCase; import makeus.cmc.malmo.application.port.in.GetLoveTypeQuestionResultUseCase; import makeus.cmc.malmo.application.port.in.GetLoveTypeQuestionsUseCase; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import org.hibernate.TypeMismatchException; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Locale; @Tag(name = "애착유형 검사 API", description = "애착유형 검사 결과 등록 API") @Slf4j +@Validated @RestController @RequestMapping("/love-types") @RequiredArgsConstructor @@ -34,6 +41,7 @@ public class LoveTypeController { private final GetLoveTypeQuestionsUseCase getLoveTypeQuestionsUseCase; private final CalculateQuestionResultUseCase calculateQuestionResultUseCase; private final GetLoveTypeQuestionResultUseCase getLoveTypeQuestionResultUseCase; + private final GetLoveTypeMbtiResultUseCase getLoveTypeMbtiResultUseCase; @Operation( summary = "애착 유형 검사 질문 조회", @@ -124,6 +132,47 @@ public BaseResponse get return BaseResponse.success(getLoveTypeQuestionResultUseCase.getResult(command)); } + @Operation( + summary = "MBTI + 애착 유형 상세 결과 조회", + description = "MBTI와 애착 유형 조합에 해당하는 상세 결과를 조회합니다." + ) + @ApiResponse( + responseCode = "200", + description = "애착 유형 상세 결과 조회 성공", + content = @Content(schema = @Schema(implementation = SwaggerResponses.LoveTypeMbtiResultSuccessResponse.class)) + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "500", + description = "서버 내부 오류", + content = @Content(schema = @Schema(implementation = SwaggerErrorResponse.class)) + ), + @ApiResponse( + responseCode = "400", + description = "잘못된 요청 또는 존재하지 않는 MBTI/애착 유형 결과", + content = @Content(schema = @Schema(implementation = SwaggerErrorResponse.class)) + ) + }) + @GetMapping("/result") + public BaseResponse getLoveTypeMbtiResult( + @RequestParam + @Pattern(regexp = "^[a-zA-Z]{4}$", message = "MBTI는 영문 4자리여야 합니다.") + String mbti, + @RequestParam("lovetype") + @Pattern( + regexp = "(?i)^(STABLE_TYPE|ANXIETY_TYPE|AVOIDANCE_TYPE|CONFUSION_TYPE)$", + message = "유효한 애착 유형이 아닙니다." + ) + String loveType + ) { + GetLoveTypeMbtiResultUseCase.GetLoveTypeMbtiResultCommand command = GetLoveTypeMbtiResultUseCase.GetLoveTypeMbtiResultCommand.builder() + .mbti(normalizeMbti(mbti)) + .loveTypeCategory(normalizeLoveTypeCategory(loveType)) + .build(); + + return BaseResponse.success(getLoveTypeMbtiResultUseCase.getResult(command)); + } + @Data public static class RegisterLoveTypeRequestDto { @Valid @@ -138,4 +187,16 @@ public static class LoveTypeTestResult { @Max(5) @Min(1) private Integer score; } + + private static String normalizeMbti(String mbti) { + return mbti == null ? null : mbti.toUpperCase(Locale.ROOT); + } + + private static LoveTypeCategory normalizeLoveTypeCategory(String loveType) { + try { + return LoveTypeCategory.valueOf(loveType.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + throw new TypeMismatchException("유효한 애착 유형이 아닙니다."); + } + } } 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 7b34fc9c..e1ba8684 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 @@ -143,6 +143,11 @@ public static class LoveTypeQuestionSuccessResponse extends BaseSwaggerResponse< public static class LoveTypeQuestionCalculateSuccessResponse extends BaseSwaggerResponse { } + @Getter + @Schema(description = "MBTI + 애착유형 상세 결과 조회 성공 응답") + public static class LoveTypeMbtiResultSuccessResponse extends BaseSwaggerResponse { + } + @Getter @Schema(description = "애착유형 등록 성공 응답") public static class RegisterLoveTypeSuccessResponse extends BaseSwaggerResponse { @@ -411,6 +416,63 @@ public static class LoveTypeQuestionCalculationData { private float anxietyRate; } + @Getter + @Schema(description = "MBTI + 애착유형 상세 결과 응답 데이터") + public static class LoveTypeMbtiResultData { + @Schema(description = "MBTI", example = "ENFP") + private String mbti; + + @Schema(description = "애착 유형", example = "STABLE_TYPE") + private LoveTypeCategory loveTypeCategory; + + @Schema(description = "요약", example = "풍부한 상상력과 사랑으로, 함께하는 일상을 즐겁게 만들어 가는 유형") + private String summary; + + @Schema(description = "키워드 목록") + private List keywords; + + @Schema(description = "강점 목록") + private List strengths; + + @Schema(description = "약점 목록") + private List weaknesses; + + @Schema(description = "관계 패턴 목록") + private List patterns; + + @Schema(description = "애착유형 특징 목록") + private List loveTypeFeatures; + + @Schema(description = "연애 가이드 목록") + private List datingGuides; + + @Schema(description = "잘 맞는 MBTI 목록") + private List bestMatches; + + @Schema(description = "부딪히기 쉬운 MBTI 목록") + private List worstMatches; + } + + @Getter + @Schema(description = "제목 + 설명 텍스트 블록") + public static class LoveTypeTextBlockData { + @Schema(description = "제목", example = "Ne") + private String title; + + @Schema(description = "설명", example = "흩어진 정보 속에서 하나의 핵심 맥락과 미래를 읽어내요") + private String description; + } + + @Getter + @Schema(description = "MBTI + 설명 블록") + public static class LoveTypeMbtiBlockData { + @Schema(description = "MBTI", example = "INFJ") + private String mbti; + + @Schema(description = "설명", example = "속마음을 깊이 이해해주며 안정적인 감정을 공유하는 궁합") + private String description; + } + @Getter @Deprecated @Schema(description = "[Deprecated] 연애 시작일 갱신 응답 데이터") diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiFeaturePersistenceAdapter.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiFeaturePersistenceAdapter.java new file mode 100644 index 00000000..1bc598e6 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiFeaturePersistenceAdapter.java @@ -0,0 +1,25 @@ +package makeus.cmc.malmo.adaptor.out.persistence.adapter; + +import lombok.RequiredArgsConstructor; +import makeus.cmc.malmo.adaptor.out.persistence.mapper.LoveTypeMbtiFeatureMapper; +import makeus.cmc.malmo.adaptor.out.persistence.repository.LoveTypeMbtiFeatureRepository; +import makeus.cmc.malmo.application.port.out.LoadLoveTypeMbtiFeaturePort; +import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiFeature; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class LoveTypeMbtiFeaturePersistenceAdapter implements LoadLoveTypeMbtiFeaturePort { + + private final LoveTypeMbtiFeatureRepository loveTypeMbtiFeatureRepository; + private final LoveTypeMbtiFeatureMapper loveTypeMbtiFeatureMapper; + + @Override + public Optional loadByMbtiAndLoveTypeCategory(String mbti, LoveTypeCategory loveTypeCategory) { + return loveTypeMbtiFeatureRepository.findByMbtiIgnoreCaseAndLoveTypeCategory(mbti, loveTypeCategory) + .map(loveTypeMbtiFeatureMapper::toDomain); + } +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypeMbtiFeatureEntity.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypeMbtiFeatureEntity.java new file mode 100644 index 00000000..1bda7855 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypeMbtiFeatureEntity.java @@ -0,0 +1,152 @@ +package makeus.cmc.malmo.adaptor.out.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import makeus.cmc.malmo.adaptor.out.persistence.entity.value.LoveTypeMbtiFeatureEntityId; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; + +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@IdClass(LoveTypeMbtiFeatureEntityId.class) +@Table(name = "love_type_mbti_feature") +public class LoveTypeMbtiFeatureEntity { + + @Id + @Column(name = "mbti") + private String mbti; + + @Id + @Column(name = "lovetype") + @Enumerated(EnumType.STRING) + private LoveTypeCategory loveTypeCategory; + + @Column(name = "summary") + private String summary; + + @Column(name = "keyword1") + private String keyword1; + + @Column(name = "keyword2") + private String keyword2; + + @Column(name = "keyword3") + private String keyword3; + + @Column(name = "strength1") + private String strength1; + + @Column(name = "strength2") + private String strength2; + + @Column(name = "strength3") + private String strength3; + + @Column(name = "weakness") + private String weakness; + + @Column(name = "strength_desc1") + private String strengthDesc1; + + @Column(name = "strength_desc2") + private String strengthDesc2; + + @Column(name = "strength_desc3") + private String strengthDesc3; + + @Column(name = "weakness_desc") + private String weaknessDesc; + + @Column(name = "pattern_title1") + private String patternTitle1; + + @Column(name = "pattern_title2") + private String patternTitle2; + + @Column(name = "pattern_title3") + private String patternTitle3; + + @Column(name = "pattern_title4") + private String patternTitle4; + + @Column(name = "pattern1") + private String pattern1; + + @Column(name = "pattern2") + private String pattern2; + + @Column(name = "pattern3") + private String pattern3; + + @Column(name = "pattern4") + private String pattern4; + + @Column(name = "lovetype_feature_title1") + private String loveTypeFeatureTitle1; + + @Column(name = "lovetype_feature_title2") + private String loveTypeFeatureTitle2; + + @Column(name = "lovetype_feature_title3") + private String loveTypeFeatureTitle3; + + @Column(name = "lovetype_feature_title4") + private String loveTypeFeatureTitle4; + + @Column(name = "lovetype_feature1") + private String loveTypeFeature1; + + @Column(name = "lovetype_feature2") + private String loveTypeFeature2; + + @Column(name = "lovetype_feature3") + private String loveTypeFeature3; + + @Column(name = "lovetype_feature4") + private String loveTypeFeature4; + + @Column(name = "dating_guide1") + private String datingGuide1; + + @Column(name = "dating_guide2") + private String datingGuide2; + + @Column(name = "dating_guide3") + private String datingGuide3; + + @Column(name = "best_mbti1") + private String bestMbti1; + + @Column(name = "best_desc1") + private String bestDesc1; + + @Column(name = "best_mbti2") + private String bestMbti2; + + @Column(name = "best_desc2") + private String bestDesc2; + + @Column(name = "worst_mbti1") + private String worstMbti1; + + @Column(name = "worst_desc1") + private String worstDesc1; + + @Column(name = "worst_mbti2") + private String worstMbti2; + + @Column(name = "worst_desc2") + private String worstDesc2; +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypeMbtiFeatureEntityId.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypeMbtiFeatureEntityId.java new file mode 100644 index 00000000..736e1e86 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypeMbtiFeatureEntityId.java @@ -0,0 +1,16 @@ +package makeus.cmc.malmo.adaptor.out.persistence.entity.value; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; + +import java.io.Serializable; + +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class LoveTypeMbtiFeatureEntityId implements Serializable { + private String mbti; + private LoveTypeCategory loveTypeCategory; +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypeMbtiFeatureMapper.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypeMbtiFeatureMapper.java new file mode 100644 index 00000000..97c1644f --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypeMbtiFeatureMapper.java @@ -0,0 +1,59 @@ +package makeus.cmc.malmo.adaptor.out.persistence.mapper; + +import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypeMbtiFeatureEntity; +import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiFeature; +import org.springframework.stereotype.Component; + +@Component +public class LoveTypeMbtiFeatureMapper { + + public LoveTypeMbtiFeature toDomain(LoveTypeMbtiFeatureEntity entity) { + if (entity == null) { + return null; + } + + return LoveTypeMbtiFeature.from( + entity.getMbti(), + entity.getLoveTypeCategory(), + entity.getSummary(), + entity.getKeyword1(), + entity.getKeyword2(), + entity.getKeyword3(), + entity.getStrength1(), + entity.getStrength2(), + entity.getStrength3(), + entity.getWeakness(), + entity.getStrengthDesc1(), + entity.getStrengthDesc2(), + entity.getStrengthDesc3(), + entity.getWeaknessDesc(), + entity.getPatternTitle1(), + entity.getPatternTitle2(), + entity.getPatternTitle3(), + entity.getPatternTitle4(), + entity.getPattern1(), + entity.getPattern2(), + entity.getPattern3(), + entity.getPattern4(), + entity.getLoveTypeFeatureTitle1(), + entity.getLoveTypeFeatureTitle2(), + entity.getLoveTypeFeatureTitle3(), + entity.getLoveTypeFeatureTitle4(), + entity.getLoveTypeFeature1(), + entity.getLoveTypeFeature2(), + entity.getLoveTypeFeature3(), + entity.getLoveTypeFeature4(), + entity.getDatingGuide1(), + entity.getDatingGuide2(), + entity.getDatingGuide3(), + entity.getBestMbti1(), + entity.getBestDesc1(), + entity.getBestMbti2(), + entity.getBestDesc2(), + entity.getWorstMbti1(), + entity.getWorstDesc1(), + entity.getWorstMbti2(), + entity.getWorstDesc2() + ); + } +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiFeatureRepository.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiFeatureRepository.java new file mode 100644 index 00000000..fae51a58 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiFeatureRepository.java @@ -0,0 +1,12 @@ +package makeus.cmc.malmo.adaptor.out.persistence.repository; + +import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypeMbtiFeatureEntity; +import makeus.cmc.malmo.adaptor.out.persistence.entity.value.LoveTypeMbtiFeatureEntityId; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface LoveTypeMbtiFeatureRepository extends JpaRepository { + Optional findByMbtiIgnoreCaseAndLoveTypeCategory(String mbti, LoveTypeCategory loveTypeCategory); +} diff --git a/src/main/java/makeus/cmc/malmo/application/exception/LoveTypeMbtiFeatureNotFoundException.java b/src/main/java/makeus/cmc/malmo/application/exception/LoveTypeMbtiFeatureNotFoundException.java new file mode 100644 index 00000000..45136057 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/exception/LoveTypeMbtiFeatureNotFoundException.java @@ -0,0 +1,4 @@ +package makeus.cmc.malmo.application.exception; + +public class LoveTypeMbtiFeatureNotFoundException extends RuntimeException { +} diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/GetLoveTypeMbtiResultUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/GetLoveTypeMbtiResultUseCase.java new file mode 100644 index 00000000..998a7e3a --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/port/in/GetLoveTypeMbtiResultUseCase.java @@ -0,0 +1,49 @@ +package makeus.cmc.malmo.application.port.in; + +import lombok.Builder; +import lombok.Data; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; + +import java.util.List; + +public interface GetLoveTypeMbtiResultUseCase { + + LoveTypeMbtiResultResponse getResult(GetLoveTypeMbtiResultCommand command); + + @Data + @Builder + class GetLoveTypeMbtiResultCommand { + private String mbti; + private LoveTypeCategory loveTypeCategory; + } + + @Data + @Builder + class LoveTypeMbtiResultResponse { + private String mbti; + private LoveTypeCategory loveTypeCategory; + private String summary; + private List keywords; + private List strengths; + private List weaknesses; + private List patterns; + private List loveTypeFeatures; + private List datingGuides; + private List bestMatches; + private List worstMatches; + } + + @Data + @Builder + class TitleDescriptionItem { + private String title; + private String description; + } + + @Data + @Builder + class MbtiDescriptionItem { + private String mbti; + private String description; + } +} diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiFeaturePort.java b/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiFeaturePort.java new file mode 100644 index 00000000..be5b9a0a --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiFeaturePort.java @@ -0,0 +1,10 @@ +package makeus.cmc.malmo.application.port.out; + +import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiFeature; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; + +import java.util.Optional; + +public interface LoadLoveTypeMbtiFeaturePort { + Optional loadByMbtiAndLoveTypeCategory(String mbti, LoveTypeCategory loveTypeCategory); +} diff --git a/src/main/java/makeus/cmc/malmo/application/service/LoveTypeMbtiFeatureService.java b/src/main/java/makeus/cmc/malmo/application/service/LoveTypeMbtiFeatureService.java new file mode 100644 index 00000000..7c89c49d --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/service/LoveTypeMbtiFeatureService.java @@ -0,0 +1,94 @@ +package makeus.cmc.malmo.application.service; + +import lombok.RequiredArgsConstructor; +import makeus.cmc.malmo.application.exception.LoveTypeMbtiFeatureNotFoundException; +import makeus.cmc.malmo.application.port.in.GetLoveTypeMbtiResultUseCase; +import makeus.cmc.malmo.application.port.out.LoadLoveTypeMbtiFeaturePort; +import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiFeature; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.stream.Stream; + +@Service +@RequiredArgsConstructor +public class LoveTypeMbtiFeatureService implements GetLoveTypeMbtiResultUseCase { + + private final LoadLoveTypeMbtiFeaturePort loadLoveTypeMbtiFeaturePort; + + @Override + public LoveTypeMbtiResultResponse getResult(GetLoveTypeMbtiResultCommand command) { + LoveTypeMbtiFeature feature = loadLoveTypeMbtiFeaturePort + .loadByMbtiAndLoveTypeCategory(command.getMbti(), command.getLoveTypeCategory()) + .orElseThrow(LoveTypeMbtiFeatureNotFoundException::new); + + return LoveTypeMbtiResultResponse.builder() + .mbti(feature.getMbti()) + .loveTypeCategory(feature.getLoveTypeCategory()) + .summary(feature.getSummary()) + .keywords(buildStringList(feature.getKeyword1(), feature.getKeyword2(), feature.getKeyword3())) + .strengths(buildTitleDescriptionItems( + feature.getStrength1(), feature.getStrengthDesc1(), + feature.getStrength2(), feature.getStrengthDesc2(), + feature.getStrength3(), feature.getStrengthDesc3() + )) + .weaknesses(buildTitleDescriptionItems(feature.getWeakness(), feature.getWeaknessDesc())) + .patterns(buildTitleDescriptionItems( + feature.getPatternTitle1(), feature.getPattern1(), + feature.getPatternTitle2(), feature.getPattern2(), + feature.getPatternTitle3(), feature.getPattern3(), + feature.getPatternTitle4(), feature.getPattern4() + )) + .loveTypeFeatures(buildTitleDescriptionItems( + feature.getLoveTypeFeatureTitle1(), feature.getLoveTypeFeature1(), + feature.getLoveTypeFeatureTitle2(), feature.getLoveTypeFeature2(), + feature.getLoveTypeFeatureTitle3(), feature.getLoveTypeFeature3(), + feature.getLoveTypeFeatureTitle4(), feature.getLoveTypeFeature4() + )) + .datingGuides(buildStringList( + feature.getDatingGuide1(), + feature.getDatingGuide2(), + feature.getDatingGuide3() + )) + .bestMatches(buildMbtiDescriptionItems( + feature.getBestMbti1(), feature.getBestDesc1(), + feature.getBestMbti2(), feature.getBestDesc2() + )) + .worstMatches(buildMbtiDescriptionItems( + feature.getWorstMbti1(), feature.getWorstDesc1(), + feature.getWorstMbti2(), feature.getWorstDesc2() + )) + .build(); + } + + private List buildStringList(String... values) { + return Stream.of(values) + .filter(StringUtils::hasText) + .toList(); + } + + private List buildTitleDescriptionItems(String... values) { + return Stream.iterate(0, index -> index < values.length, index -> index + 2) + .map(index -> TitleDescriptionItem.builder() + .title(normalizeBlank(values[index])) + .description(normalizeBlank(values[index + 1])) + .build()) + .filter(item -> StringUtils.hasText(item.getTitle()) || StringUtils.hasText(item.getDescription())) + .toList(); + } + + private List buildMbtiDescriptionItems(String... values) { + return Stream.iterate(0, index -> index < values.length, index -> index + 2) + .map(index -> MbtiDescriptionItem.builder() + .mbti(normalizeBlank(values[index])) + .description(normalizeBlank(values[index + 1])) + .build()) + .filter(item -> StringUtils.hasText(item.getMbti()) || StringUtils.hasText(item.getDescription())) + .toList(); + } + + private String normalizeBlank(String value) { + return StringUtils.hasText(value) ? value : null; + } +} diff --git a/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypeMbtiFeature.java b/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypeMbtiFeature.java new file mode 100644 index 00000000..954baba7 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypeMbtiFeature.java @@ -0,0 +1,144 @@ +package makeus.cmc.malmo.domain.model.love_type; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; + +@Getter +@Builder(access = AccessLevel.PRIVATE) +public class LoveTypeMbtiFeature { + private String mbti; + private LoveTypeCategory loveTypeCategory; + private String summary; + private String keyword1; + private String keyword2; + private String keyword3; + private String strength1; + private String strength2; + private String strength3; + private String weakness; + private String strengthDesc1; + private String strengthDesc2; + private String strengthDesc3; + private String weaknessDesc; + private String patternTitle1; + private String patternTitle2; + private String patternTitle3; + private String patternTitle4; + private String pattern1; + private String pattern2; + private String pattern3; + private String pattern4; + private String loveTypeFeatureTitle1; + private String loveTypeFeatureTitle2; + private String loveTypeFeatureTitle3; + private String loveTypeFeatureTitle4; + private String loveTypeFeature1; + private String loveTypeFeature2; + private String loveTypeFeature3; + private String loveTypeFeature4; + private String datingGuide1; + private String datingGuide2; + private String datingGuide3; + private String bestMbti1; + private String bestDesc1; + private String bestMbti2; + private String bestDesc2; + private String worstMbti1; + private String worstDesc1; + private String worstMbti2; + private String worstDesc2; + + public static LoveTypeMbtiFeature from( + String mbti, + LoveTypeCategory loveTypeCategory, + String summary, + String keyword1, + String keyword2, + String keyword3, + String strength1, + String strength2, + String strength3, + String weakness, + String strengthDesc1, + String strengthDesc2, + String strengthDesc3, + String weaknessDesc, + String patternTitle1, + String patternTitle2, + String patternTitle3, + String patternTitle4, + String pattern1, + String pattern2, + String pattern3, + String pattern4, + String loveTypeFeatureTitle1, + String loveTypeFeatureTitle2, + String loveTypeFeatureTitle3, + String loveTypeFeatureTitle4, + String loveTypeFeature1, + String loveTypeFeature2, + String loveTypeFeature3, + String loveTypeFeature4, + String datingGuide1, + String datingGuide2, + String datingGuide3, + String bestMbti1, + String bestDesc1, + String bestMbti2, + String bestDesc2, + String worstMbti1, + String worstDesc1, + String worstMbti2, + String worstDesc2 + ) { + return LoveTypeMbtiFeature.builder() + .mbti(normalizeMbti(mbti)) + .loveTypeCategory(loveTypeCategory) + .summary(summary) + .keyword1(keyword1) + .keyword2(keyword2) + .keyword3(keyword3) + .strength1(strength1) + .strength2(strength2) + .strength3(strength3) + .weakness(weakness) + .strengthDesc1(strengthDesc1) + .strengthDesc2(strengthDesc2) + .strengthDesc3(strengthDesc3) + .weaknessDesc(weaknessDesc) + .patternTitle1(patternTitle1) + .patternTitle2(patternTitle2) + .patternTitle3(patternTitle3) + .patternTitle4(patternTitle4) + .pattern1(pattern1) + .pattern2(pattern2) + .pattern3(pattern3) + .pattern4(pattern4) + .loveTypeFeatureTitle1(loveTypeFeatureTitle1) + .loveTypeFeatureTitle2(loveTypeFeatureTitle2) + .loveTypeFeatureTitle3(loveTypeFeatureTitle3) + .loveTypeFeatureTitle4(loveTypeFeatureTitle4) + .loveTypeFeature1(loveTypeFeature1) + .loveTypeFeature2(loveTypeFeature2) + .loveTypeFeature3(loveTypeFeature3) + .loveTypeFeature4(loveTypeFeature4) + .datingGuide1(datingGuide1) + .datingGuide2(datingGuide2) + .datingGuide3(datingGuide3) + .bestMbti1(normalizeMbti(bestMbti1)) + .bestDesc1(bestDesc1) + .bestMbti2(normalizeMbti(bestMbti2)) + .bestDesc2(bestDesc2) + .worstMbti1(normalizeMbti(worstMbti1)) + .worstDesc1(worstDesc1) + .worstMbti2(normalizeMbti(worstMbti2)) + .worstDesc2(worstDesc2) + .build(); + } + + private static String normalizeMbti(String mbti) { + return mbti == null ? null : mbti.toUpperCase(); + } +} diff --git a/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeQuestionTest.java b/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeQuestionTest.java index 7da77952..eea92dda 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeQuestionTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeQuestionTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; import jakarta.persistence.EntityManager; +import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypeMbtiFeatureEntity; import makeus.cmc.malmo.adaptor.out.persistence.entity.TempLoveTypeEntity; import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; import makeus.cmc.malmo.integration_test.dto_factory.LoveTypeQuestionRequestDtoFactory; @@ -248,4 +249,118 @@ class GetLoveTypeResultTest { } } + + @Nested + @DisplayName("MBTI + 애착 유형 상세 결과 조회 테스트") + class GetLoveTypeMbtiResultTest { + @Test + @DisplayName("MBTI와 애착 유형 상세 결과 조회 성공 - 소문자 쿼리와 빈 항목 제외") + void mbti와_애착유형_상세_결과_조회_성공() throws Exception { + // given + em.persist(LoveTypeMbtiFeatureEntity.builder() + .mbti("ENFP") + .loveTypeCategory(LoveTypeCategory.STABLE_TYPE) + .summary("풍부한 상상력과 사랑으로, 함께하는 일상을 즐겁게 만들어 가는 유형") + .keyword1("열정적") + .keyword2("자유로움") + .keyword3("") + .strength1("Ne") + .strengthDesc1("흩어진 정보 속에서 하나의 핵심 맥락과 미래를 읽어내요") + .strength2("") + .strengthDesc2("") + .strength3(null) + .strengthDesc3(null) + .weakness("Si") + .weaknessDesc("반복되는 루틴에 답답함을 느껴요") + .patternTitle1("나다운 기준을 지켜요") + .pattern1("여러 가능성 속에서 무엇이 나에게 의미 있는지 먼저 생각해요") + .patternTitle2("") + .pattern2("") + .patternTitle3(null) + .pattern3(null) + .patternTitle4(null) + .pattern4(null) + .loveTypeFeatureTitle1("미래의 가능성을 자주 상상해요") + .loveTypeFeature1("연인과 함께할 미래의 가능성을 상상하며 창의적인 질문으로 관계를 만들어요") + .loveTypeFeatureTitle2("") + .loveTypeFeature2("") + .loveTypeFeatureTitle3(null) + .loveTypeFeature3(null) + .loveTypeFeatureTitle4(null) + .loveTypeFeature4(null) + .datingGuide1("감정을 정리해 표현해요") + .datingGuide2("") + .datingGuide3(null) + .bestMbti1("infj") + .bestDesc1("속마음을 깊이 이해해주며 안정적인 감정을 공유하는 궁합") + .bestMbti2("") + .bestDesc2("") + .worstMbti1("istp") + .worstDesc1("자유로운 감정선과 솔직한 피드백이 부딪히는 궁합") + .worstMbti2(null) + .worstDesc2(null) + .build()); + em.flush(); + em.clear(); + + // when & then + mockMvc.perform(get("/love-types/result") + .param("mbti", "enfp") + .param("lovetype", "stable_type") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("data.mbti").value("ENFP")) + .andExpect(jsonPath("data.loveTypeCategory").value(LoveTypeCategory.STABLE_TYPE.name())) + .andExpect(jsonPath("data.summary").value("풍부한 상상력과 사랑으로, 함께하는 일상을 즐겁게 만들어 가는 유형")) + .andExpect(jsonPath("data.keywords.length()").value(2)) + .andExpect(jsonPath("data.keywords[0]").value("열정적")) + .andExpect(jsonPath("data.strengths.length()").value(1)) + .andExpect(jsonPath("data.strengths[0].title").value("Ne")) + .andExpect(jsonPath("data.weaknesses.length()").value(1)) + .andExpect(jsonPath("data.weaknesses[0].title").value("Si")) + .andExpect(jsonPath("data.patterns.length()").value(1)) + .andExpect(jsonPath("data.loveTypeFeatures.length()").value(1)) + .andExpect(jsonPath("data.datingGuides.length()").value(1)) + .andExpect(jsonPath("data.bestMatches.length()").value(1)) + .andExpect(jsonPath("data.bestMatches[0].mbti").value("INFJ")) + .andExpect(jsonPath("data.worstMatches.length()").value(1)) + .andExpect(jsonPath("data.worstMatches[0].mbti").value("ISTP")); + } + + @Test + @DisplayName("MBTI와 애착 유형 상세 결과 조회 실패 - MBTI 형식 오류") + void mbti와_애착유형_상세_결과_조회_실패_mbti형식오류() throws Exception { + mockMvc.perform(get("/love-types/result") + .param("mbti", "ENF") + .param("lovetype", "STABLE_TYPE") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("message").value(BAD_REQUEST.getMessage())) + .andExpect(jsonPath("code").value(BAD_REQUEST.getCode())); + } + + @Test + @DisplayName("MBTI와 애착 유형 상세 결과 조회 실패 - 애착 유형 값 오류") + void mbti와_애착유형_상세_결과_조회_실패_애착유형값오류() throws Exception { + mockMvc.perform(get("/love-types/result") + .param("mbti", "ENFP") + .param("lovetype", "WRONG_TYPE") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("message").value(BAD_REQUEST.getMessage())) + .andExpect(jsonPath("code").value(BAD_REQUEST.getCode())); + } + + @Test + @DisplayName("MBTI와 애착 유형 상세 결과 조회 실패 - 매칭되는 결과 없음") + void mbti와_애착유형_상세_결과_조회_실패_결과없음() throws Exception { + mockMvc.perform(get("/love-types/result") + .param("mbti", "ENFP") + .param("lovetype", "STABLE_TYPE") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("message").value(NO_SUCH_LOVE_TYPE_MBTI_RESULT.getMessage())) + .andExpect(jsonPath("code").value(NO_SUCH_LOVE_TYPE_MBTI_RESULT.getCode())); + } + } } From f7dacde50e51b9f8eba7e1f2f857bdbb778bcefb Mon Sep 17 00:00:00 2001 From: Choi wontak <76509639+RoundTable02@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:46:35 +0900 Subject: [PATCH 04/15] =?UTF-8?q?feat:=20mbti=20=EC=95=A0=EC=B0=A9?= =?UTF-8?q?=EC=9C=A0=ED=98=95=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20=ED=94=84?= =?UTF-8?q?=EB=A1=AC=ED=94=84=ED=8A=B8=EC=97=90=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/API-CHANGES-CHAT-PROMPT-MBTI-LOVETYPE.md | 137 ++++++++++++ docs/API-CHANGES-LOVETYPE-DATA.md | 28 ++- docs/CHAT-PROMPT-MBTI-LOVETYPE-EXAMPLE.md | 138 ++++++++++++ sqls/MM-181.sql | 15 ++ .../LoveTypeMbtiPromptPersistenceAdapter.java | 25 +++ .../entity/LoveTypeMbtiPromptEntity.java | 38 ++++ .../value/LoveTypeMbtiPromptEntityId.java | 16 ++ .../mapper/LoveTypeMbtiPromptMapper.java | 21 ++ .../LoveTypeMbtiPromptRepository.java | 12 ++ .../LoveTypeMbtiPromptQueryHelper.java | 20 ++ .../port/out/LoadLoveTypeMbtiPromptPort.java | 10 + .../service/chat/ChatPromptBuilder.java | 54 +++++ .../model/love_type/LoveTypeMbtiPrompt.java | 26 +++ .../service/chat/ChatPromptBuilderTest.java | 199 ++++++++++++++++++ ...eTypeMbtiPromptPersistenceAdapterTest.java | 50 +++++ 15 files changed, 788 insertions(+), 1 deletion(-) create mode 100644 docs/API-CHANGES-CHAT-PROMPT-MBTI-LOVETYPE.md create mode 100644 docs/CHAT-PROMPT-MBTI-LOVETYPE-EXAMPLE.md create mode 100644 sqls/MM-181.sql create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiPromptPersistenceAdapter.java create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypeMbtiPromptEntity.java create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypeMbtiPromptEntityId.java create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypeMbtiPromptMapper.java create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiPromptRepository.java create mode 100644 src/main/java/makeus/cmc/malmo/application/helper/love_type/LoveTypeMbtiPromptQueryHelper.java create mode 100644 src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiPromptPort.java create mode 100644 src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypeMbtiPrompt.java create mode 100644 src/test/java/makeus/cmc/malmo/application/service/chat/ChatPromptBuilderTest.java create mode 100644 src/test/java/makeus/cmc/malmo/integration_test/LoveTypeMbtiPromptPersistenceAdapterTest.java diff --git a/docs/API-CHANGES-CHAT-PROMPT-MBTI-LOVETYPE.md b/docs/API-CHANGES-CHAT-PROMPT-MBTI-LOVETYPE.md new file mode 100644 index 00000000..89c4c3a9 --- /dev/null +++ b/docs/API-CHANGES-CHAT-PROMPT-MBTI-LOVETYPE.md @@ -0,0 +1,137 @@ +# 채팅 프롬프트 변경 사항 - MBTI + 애착유형 조합 프롬프트 주입 + +## 개요 + +채팅 시스템 메시지의 `[사용자 메타데이터]` 블록에 사용자와 상대방의 `MBTI + 애착유형` 조합별 프롬프트를 함께 주입합니다. + +이 변경의 목적은 다음과 같습니다. + +1. 상담 모델이 사용자 성향과 상대방 성향을 더 구체적으로 이해하도록 돕기 +2. `UNKNOWN` 상태를 명시적으로 전달해 대화 문맥으로 추론하게 만들기 +3. 조합 데이터가 일부 비어 있어도 채팅을 실패시키지 않고 안전하게 계속 진행하기 + +--- + +## 데이터 소스 + +### 신규 테이블 + +`love_type_mbti_prompt` + +| column | type | description | +| --- | --- | --- | +| `mbti` | `VARCHAR(4)` | MBTI 4자리 문자열 | +| `lovetype` | `VARCHAR(255)` | `LoveTypeCategory` enum 문자열 | +| `prompts` | `TEXT` | 채팅 메타데이터에 삽입할 프롬프트 전문 | + +### 키 규칙 + +- `(mbti, lovetype)` 복합 PK +- 애플리케이션은 MBTI를 대문자로 정규화해 조회합니다. +- `UNKNOWN` row는 저장하지 않습니다. + +### DDL + +```sql +CREATE TABLE love_type_mbti_prompt ( + mbti VARCHAR(4) NOT NULL, + lovetype VARCHAR(255) NOT NULL, + prompts TEXT, + PRIMARY KEY (mbti, lovetype) +); +``` + +실제 배포용 SQL은 `sqls/MM-181.sql`에 있습니다. + +--- + +## 채팅 메타데이터 주입 규칙 + +프롬프트는 `ChatPromptBuilder`에서 `[사용자 메타데이터]` 문자열을 만들 때 삽입됩니다. + +### 추가되는 항목 + +```text +- 사용자 성향 프롬프트: +{사용자 조합 프롬프트 또는 폴백 문구} + +- 상대방 성향 프롬프트: +{상대방 조합 프롬프트 또는 폴백 문구} +``` + +### 사용자 본인 + +- `member.mbti`와 `member.loveTypeCategory`가 모두 있으면 `love_type_mbti_prompt`를 조회합니다. +- 조합 row가 존재하면 `prompts` 컬럼 값을 그대로 삽입합니다. +- 둘 중 하나라도 없거나 조합 row가 없으면 아래 폴백 문구를 삽입합니다. + +```text +UNKNOWN, 사용자와의 대화로부터 유추할 것 +``` + +### 상대방 + +- `partnerMbti`가 있는 경우에만 `- 상대방 성향 프롬프트:` 항목을 추가합니다. +- `partnerLoveTypeCategory`가 `UNKNOWN` 또는 `null`이면 DB 조회 없이 바로 폴백 문구를 삽입합니다. +- `partnerMbti`와 확정된 `partnerLoveTypeCategory`가 모두 있으면 `(partnerMbti, partnerLoveTypeCategory)` 조합으로 조회합니다. +- 조합 row가 없으면 채팅은 실패시키지 않고 동일한 폴백 문구를 삽입합니다. + +--- + +## 런타임 동작 + +### 성공 케이스 + +- 사용자 조합 row가 있으면 사용자 성향 프롬프트에 해당 전문이 들어갑니다. +- 상대방 조합 row가 있으면 상대방 성향 프롬프트에 해당 전문이 들어갑니다. + +### 폴백 케이스 + +아래 경우에는 모두 동일한 폴백 문구를 사용합니다. + +- 사용자 MBTI 없음 +- 사용자 애착유형 없음 +- 상대방 MBTI 없음 +- 상대방 애착유형이 `UNKNOWN` +- 상대방 애착유형이 `null` +- 조합 row 없음 +- `prompts` 값이 비어 있음 + +### 로깅 + +- MBTI/애착유형 값은 존재하지만 조합 row가 없는 경우 `warn` 로그를 남깁니다. +- 이 경우에도 채팅 요청은 실패하지 않습니다. + +--- + +## 외부 API 영향 + +외부 HTTP API 스펙 변경은 없습니다. + +다만 아래 프로필 데이터가 채팅 프롬프트 생성에 직접 활용됩니다. + +- `GET /members`의 `mbti` +- `GET /members`의 `loveTypeCategory` +- `GET /members`의 `partnerMbti` +- `GET /members`의 `partnerLoveTypeCategory` + +--- + +## 테스트 + +검증한 항목: + +- MBTI 대소문자 무관 조회 +- 사용자 조합 프롬프트 삽입 +- 사용자 정보 없음 시 폴백 문구 삽입 +- 상대방 조합 프롬프트 삽입 +- 상대방 애착유형 `UNKNOWN` 시 폴백 문구 삽입 +- 상대방 프로필 미등록 시 상대방 성향 프롬프트 항목 생략 +- 조합 row 누락 시 예외 없이 폴백 처리 + +관련 테스트: + +- `src/test/java/makeus/cmc/malmo/application/service/chat/ChatPromptBuilderTest.java` +- `src/test/java/makeus/cmc/malmo/integration_test/LoveTypeMbtiPromptPersistenceAdapterTest.java` + +실제 시스템 메시지 예시는 `docs/CHAT-PROMPT-MBTI-LOVETYPE-EXAMPLE.md`를 참고합니다. diff --git a/docs/API-CHANGES-LOVETYPE-DATA.md b/docs/API-CHANGES-LOVETYPE-DATA.md index 6e3dbf1c..1a439930 100644 --- a/docs/API-CHANGES-LOVETYPE-DATA.md +++ b/docs/API-CHANGES-LOVETYPE-DATA.md @@ -52,4 +52,30 @@ partnerLoveTypeCategory: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE' | 'UNKNOWN' // undefined = 미입력 / UNKNOWN = "모르겠어요" 선택됨 } -``` \ No newline at end of file +``` + +--- + +## 채팅 프롬프트 활용 + +사용자와 상대방 프로필의 `mbti`, `loveTypeCategory`, `partnerMbti`, `partnerLoveTypeCategory`는 채팅 시스템 메시지의 메타데이터 구성에도 사용됩니다. + +### 활용 규칙 + +- 사용자 본인 + - `mbti`와 `loveTypeCategory`가 모두 있으면 `(mbti, lovetype)` 조합으로 상세 프롬프트를 조회합니다. + - 둘 중 하나라도 없거나 매칭 row가 없으면 `UNKNOWN, 사용자와의 대화로부터 유추할 것`을 사용합니다. +- 상대방 + - `partnerMbti`가 있을 때만 상대방 성향 프롬프트 항목이 추가됩니다. + - `partnerLoveTypeCategory`가 `UNKNOWN` 또는 `null`이면 DB 조회 없이 `UNKNOWN, 사용자와의 대화로부터 유추할 것`을 사용합니다. + - `partnerMbti`와 확정된 `partnerLoveTypeCategory`가 모두 있으면 `(partnerMbti, partnerLoveTypeCategory)` 조합으로 상세 프롬프트를 조회합니다. + - 매칭 row가 없으면 채팅은 실패하지 않고 동일한 폴백 문구를 사용합니다. + +### 조회 대상 테이블 + +- `love_type_mbti_prompt` + - `mbti`: MBTI 4자리 문자열 + - `lovetype`: `STABLE_TYPE | ANXIETY_TYPE | AVOIDANCE_TYPE | CONFUSION_TYPE` + - `prompts`: 실제 채팅 메타데이터에 삽입할 프롬프트 전문 + +상세 동작 예시는 `docs/API-CHANGES-CHAT-PROMPT-MBTI-LOVETYPE.md`, 실제 전달 예시는 `docs/CHAT-PROMPT-MBTI-LOVETYPE-EXAMPLE.md`를 참고합니다. diff --git a/docs/CHAT-PROMPT-MBTI-LOVETYPE-EXAMPLE.md b/docs/CHAT-PROMPT-MBTI-LOVETYPE-EXAMPLE.md new file mode 100644 index 00000000..1d55716d --- /dev/null +++ b/docs/CHAT-PROMPT-MBTI-LOVETYPE-EXAMPLE.md @@ -0,0 +1,138 @@ +# 채팅 프롬프트 예시 - MBTI + 애착유형 메타데이터 주입 + +## 목적 + +이 문서는 실제 채팅 요청 시 모델에 전달되는 메타데이터가 어떤 형태인지 예시를 보여줍니다. + +주의: + +- 아래 예시는 `ChatPromptBuilder`가 만드는 메시지 중 첫 번째 `system` 메시지의 예시입니다. +- 실제 요청에는 이 메타데이터 뒤에 기존 대화 이력, 단계별 요약, 현재 사용자 메시지가 순서대로 붙습니다. + +--- + +## 예시 1. 사용자/상대방 모두 조합 데이터가 있는 경우 + +가정: + +- 사용자 MBTI: `ISTJ` +- 사용자 애착유형: `STABLE_TYPE` +- 상대방 MBTI: `ENFP` +- 상대방 애착유형: `ANXIETY_TYPE` +- 두 조합 모두 `love_type_mbti_prompt` 테이블에 row 존재 + +실제로 모델에 들어가는 첫 system 메시지 예시: + +```text +[사용자 메타데이터] +- 사용자 이름: 다은 +- 사용자 연애 상태: IN_RELATIONSHIP +- 사용자 MBTI: ISTJ +- 상대방 MBTI: ENFP +- 사용자 애착 유형: 안정형 +- 애인 애착 유형: 불안형 +- 사용자 성향 프롬프트: +ISTJ 안정형 + +# 애착 결합 행동 패턴 + +약속과 책임을 최우선하며 일관된 행동으로... + +- 상대방 성향 프롬프트: +ENFP 불안형 + +# 애착 결합 행동 패턴 + +감정적 연결이 흔들린다고 느끼면... +``` + +--- + +## 예시 2. 상대방 애착유형이 UNKNOWN인 경우 + +가정: + +- 사용자 MBTI: `ISTJ` +- 사용자 애착유형: `STABLE_TYPE` +- 상대방 MBTI: `ENFP` +- 상대방 애착유형: `UNKNOWN` + +```text +[사용자 메타데이터] +- 사용자 이름: 다은 +- 사용자 연애 상태: IN_RELATIONSHIP +- 사용자 MBTI: ISTJ +- 상대방 MBTI: ENFP +- 사용자 애착 유형: 안정형 +- 애인 애착 유형: 모르겠어요 +- 사용자 성향 프롬프트: +ISTJ 안정형 + +# 애착 결합 행동 패턴 + +약속과 책임을 최우선하며... + +- 상대방 성향 프롬프트: +UNKNOWN, 사용자와의 대화로부터 유추할 것 +``` + +핵심: + +- `partnerMbti`가 있으므로 상대방 성향 프롬프트 항목은 생성됩니다. +- `partnerLoveTypeCategory == UNKNOWN` 이므로 DB 조회 없이 폴백 문구가 들어갑니다. + +--- + +## 예시 3. 사용자 조합 row가 아직 없는 경우 + +가정: + +- 사용자 MBTI: `INFP` +- 사용자 애착유형: `CONFUSION_TYPE` +- `love_type_mbti_prompt`에 `(INFP, CONFUSION_TYPE)` row 없음 + +```text +[사용자 메타데이터] +- 사용자 이름: 다은 +- 사용자 연애 상태: SEEING_SOMEONE +- 사용자 MBTI: INFP +- 상대방 MBTI: 알 수 없음 +- 사용자 애착 유형: 혼란형 +- 애인 애착 유형: 알 수 없음 +- 사용자 성향 프롬프트: +UNKNOWN, 사용자와의 대화로부터 유추할 것 +``` + +핵심: + +- 사용자 프로필 값은 있어도 row가 없으면 채팅을 실패시키지 않습니다. +- 애플리케이션은 `warn` 로그를 남기고 폴백 문구를 삽입합니다. + +--- + +## OpenAI 요청 메시지 배열 예시 + +실제 요청 직전 메시지 배열은 대략 아래 순서가 됩니다. + +```json +[ + { + "role": "system", + "content": "[사용자 메타데이터] ..." + }, + { + "role": "system", + "content": "[사용자의 갈등 내용] ..." + }, + { + "role": "assistant", + "content": "이전 상담 응답" + }, + { + "role": "user", + "content": "제가 먼저 연락해야 할지 모르겠어요." + } +] +``` + +즉, 이번 기능은 별도 API 필드를 추가하는 방식이 아니라, 모델에 전달되는 첫 `system` 메시지의 메타데이터 내용을 더 풍부하게 만드는 방식으로 동작합니다. diff --git a/sqls/MM-181.sql b/sqls/MM-181.sql new file mode 100644 index 00000000..1c3463cd --- /dev/null +++ b/sqls/MM-181.sql @@ -0,0 +1,15 @@ +-- MM-181: Add love_type_mbti_prompt table for chat prompt enrichment +-- Author: Codex +-- Date: 2026-03-16 + +CREATE TABLE love_type_mbti_prompt ( + mbti VARCHAR(4) NOT NULL, + lovetype VARCHAR(255) NOT NULL, + prompts TEXT, + PRIMARY KEY (mbti, lovetype) +); + +COMMENT ON TABLE love_type_mbti_prompt IS 'MBTI and love type specific prompt snippets for chat metadata'; +COMMENT ON COLUMN love_type_mbti_prompt.mbti IS 'MBTI in 4-letter uppercase format'; +COMMENT ON COLUMN love_type_mbti_prompt.lovetype IS 'Love type category: STABLE_TYPE, ANXIETY_TYPE, AVOIDANCE_TYPE, CONFUSION_TYPE'; +COMMENT ON COLUMN love_type_mbti_prompt.prompts IS 'Prompt content to inject into chat metadata'; diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiPromptPersistenceAdapter.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiPromptPersistenceAdapter.java new file mode 100644 index 00000000..700e715c --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiPromptPersistenceAdapter.java @@ -0,0 +1,25 @@ +package makeus.cmc.malmo.adaptor.out.persistence.adapter; + +import lombok.RequiredArgsConstructor; +import makeus.cmc.malmo.adaptor.out.persistence.mapper.LoveTypeMbtiPromptMapper; +import makeus.cmc.malmo.adaptor.out.persistence.repository.LoveTypeMbtiPromptRepository; +import makeus.cmc.malmo.application.port.out.LoadLoveTypeMbtiPromptPort; +import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiPrompt; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class LoveTypeMbtiPromptPersistenceAdapter implements LoadLoveTypeMbtiPromptPort { + + private final LoveTypeMbtiPromptRepository loveTypeMbtiPromptRepository; + private final LoveTypeMbtiPromptMapper loveTypeMbtiPromptMapper; + + @Override + public Optional loadByMbtiAndLoveTypeCategory(String mbti, LoveTypeCategory loveTypeCategory) { + return loveTypeMbtiPromptRepository.findByMbtiIgnoreCaseAndLoveTypeCategory(mbti, loveTypeCategory) + .map(loveTypeMbtiPromptMapper::toDomain); + } +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypeMbtiPromptEntity.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypeMbtiPromptEntity.java new file mode 100644 index 00000000..941c896b --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypeMbtiPromptEntity.java @@ -0,0 +1,38 @@ +package makeus.cmc.malmo.adaptor.out.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import makeus.cmc.malmo.adaptor.out.persistence.entity.value.LoveTypeMbtiPromptEntityId; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; + +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@IdClass(LoveTypeMbtiPromptEntityId.class) +@Table(name = "love_type_mbti_prompt") +public class LoveTypeMbtiPromptEntity { + + @Id + @Column(name = "mbti") + private String mbti; + + @Id + @Column(name = "lovetype") + @Enumerated(EnumType.STRING) + private LoveTypeCategory loveTypeCategory; + + @Column(name = "prompts", columnDefinition = "TEXT") + private String prompts; +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypeMbtiPromptEntityId.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypeMbtiPromptEntityId.java new file mode 100644 index 00000000..df098da3 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypeMbtiPromptEntityId.java @@ -0,0 +1,16 @@ +package makeus.cmc.malmo.adaptor.out.persistence.entity.value; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; + +import java.io.Serializable; + +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class LoveTypeMbtiPromptEntityId implements Serializable { + private String mbti; + private LoveTypeCategory loveTypeCategory; +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypeMbtiPromptMapper.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypeMbtiPromptMapper.java new file mode 100644 index 00000000..42120aec --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypeMbtiPromptMapper.java @@ -0,0 +1,21 @@ +package makeus.cmc.malmo.adaptor.out.persistence.mapper; + +import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypeMbtiPromptEntity; +import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiPrompt; +import org.springframework.stereotype.Component; + +@Component +public class LoveTypeMbtiPromptMapper { + + public LoveTypeMbtiPrompt toDomain(LoveTypeMbtiPromptEntity entity) { + if (entity == null) { + return null; + } + + return LoveTypeMbtiPrompt.from( + entity.getMbti(), + entity.getLoveTypeCategory(), + entity.getPrompts() + ); + } +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiPromptRepository.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiPromptRepository.java new file mode 100644 index 00000000..b778c472 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiPromptRepository.java @@ -0,0 +1,12 @@ +package makeus.cmc.malmo.adaptor.out.persistence.repository; + +import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypeMbtiPromptEntity; +import makeus.cmc.malmo.adaptor.out.persistence.entity.value.LoveTypeMbtiPromptEntityId; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface LoveTypeMbtiPromptRepository extends JpaRepository { + Optional findByMbtiIgnoreCaseAndLoveTypeCategory(String mbti, LoveTypeCategory loveTypeCategory); +} diff --git a/src/main/java/makeus/cmc/malmo/application/helper/love_type/LoveTypeMbtiPromptQueryHelper.java b/src/main/java/makeus/cmc/malmo/application/helper/love_type/LoveTypeMbtiPromptQueryHelper.java new file mode 100644 index 00000000..d57950fd --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/helper/love_type/LoveTypeMbtiPromptQueryHelper.java @@ -0,0 +1,20 @@ +package makeus.cmc.malmo.application.helper.love_type; + +import lombok.RequiredArgsConstructor; +import makeus.cmc.malmo.application.port.out.LoadLoveTypeMbtiPromptPort; +import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiPrompt; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class LoveTypeMbtiPromptQueryHelper { + + private final LoadLoveTypeMbtiPromptPort loadLoveTypeMbtiPromptPort; + + public Optional findByMbtiAndLoveTypeCategory(String mbti, LoveTypeCategory loveTypeCategory) { + return loadLoveTypeMbtiPromptPort.loadByMbtiAndLoveTypeCategory(mbti, loveTypeCategory); + } +} diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiPromptPort.java b/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiPromptPort.java new file mode 100644 index 00000000..808c5dc2 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiPromptPort.java @@ -0,0 +1,10 @@ +package makeus.cmc.malmo.application.port.out; + +import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiPrompt; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; + +import java.util.Optional; + +public interface LoadLoveTypeMbtiPromptPort { + Optional loadByMbtiAndLoveTypeCategory(String mbti, LoveTypeCategory loveTypeCategory); +} 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 af9358b3..fcc40d15 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 @@ -1,8 +1,10 @@ package makeus.cmc.malmo.application.service.chat; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import makeus.cmc.malmo.application.helper.chat_room.ChatRoomQueryHelper; import makeus.cmc.malmo.application.helper.chat_room.MemberChatRoomMetadataQueryHelper; +import makeus.cmc.malmo.application.helper.love_type.LoveTypeMbtiPromptQueryHelper; import makeus.cmc.malmo.application.port.out.chat.LoadChatRoomMetadataPort; import makeus.cmc.malmo.domain.model.chat.ChatMessage; import makeus.cmc.malmo.domain.model.chat.ChatRoom; @@ -12,19 +14,26 @@ import makeus.cmc.malmo.domain.model.member.MemberMemory; import makeus.cmc.malmo.domain.value.id.ChatRoomId; import makeus.cmc.malmo.domain.value.id.MemberId; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; import makeus.cmc.malmo.domain.value.type.SenderType; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.List; import java.util.Map; +@Slf4j @Service @RequiredArgsConstructor public class ChatPromptBuilder { + private static final String UNKNOWN_INFERENCE_PROMPT = "UNKNOWN, 사용자와의 대화로부터 유추할 것"; + private final ChatRoomQueryHelper chatRoomQueryHelper; private final MemberChatRoomMetadataQueryHelper memberChatRoomMetadataQueryHelper; + private final LoveTypeMbtiPromptQueryHelper loveTypeMbtiPromptQueryHelper; public List> createForProcessUserMessage(Member member, ChatRoom chatRoom, String userMessage) { List> messages = new ArrayList<>(); @@ -96,11 +105,56 @@ private String getMetaDataContent(Member member) { String partnerLoveType = chatRoomMetadataDto.partnerLoveType() != null ? chatRoomMetadataDto.partnerLoveType().getDescription() : "알 수 없음"; metadataBuilder.append("- 애인 애착 유형: ").append(partnerLoveType).append("\n"); + metadataBuilder.append("- 사용자 성향 프롬프트:\n") + .append(resolveMemberPrompt(member)) + .append("\n"); + + if (StringUtils.hasText(member.getPartnerMbti())) { + metadataBuilder.append("- 상대방 성향 프롬프트:\n") + .append(resolvePartnerPrompt(member)) + .append("\n"); + } + metadataBuilder.append(memberMemoryList); return metadataBuilder.toString(); } + private String resolveMemberPrompt(Member member) { + if (!StringUtils.hasText(member.getMbti()) || member.getLoveTypeCategory() == null) { + return UNKNOWN_INFERENCE_PROMPT; + } + + return loveTypeMbtiPromptQueryHelper.findByMbtiAndLoveTypeCategory(member.getMbti(), member.getLoveTypeCategory()) + .map(prompt -> prompt.getPrompts()) + .filter(StringUtils::hasText) + .orElseGet(() -> { + log.warn("Missing love_type_mbti_prompt row for member. mbti={}, loveType={}", + member.getMbti(), member.getLoveTypeCategory()); + return UNKNOWN_INFERENCE_PROMPT; + }); + } + + private String resolvePartnerPrompt(Member member) { + if (!StringUtils.hasText(member.getPartnerMbti())) { + return UNKNOWN_INFERENCE_PROMPT; + } + + if (member.getPartnerLoveTypeCategory() == null || member.getPartnerLoveTypeCategory() == PartnerLoveTypeCategory.UNKNOWN) { + return UNKNOWN_INFERENCE_PROMPT; + } + + LoveTypeCategory partnerLoveTypeCategory = LoveTypeCategory.valueOf(member.getPartnerLoveTypeCategory().name()); + return loveTypeMbtiPromptQueryHelper.findByMbtiAndLoveTypeCategory(member.getPartnerMbti(), partnerLoveTypeCategory) + .map(prompt -> prompt.getPrompts()) + .filter(StringUtils::hasText) + .orElseGet(() -> { + log.warn("Missing love_type_mbti_prompt row for partner. memberId={}, partnerMbti={}, partnerLoveType={}", + member.getId(), member.getPartnerMbti(), partnerLoveTypeCategory); + return UNKNOWN_INFERENCE_PROMPT; + }); + } + public String getMemberMemoriesByMemberId(MemberId memberId) { StringBuilder sb = new StringBuilder(); List memoryList = chatRoomQueryHelper.getMemberMemoriesByMemberId(memberId); diff --git a/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypeMbtiPrompt.java b/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypeMbtiPrompt.java new file mode 100644 index 00000000..b933c16c --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypeMbtiPrompt.java @@ -0,0 +1,26 @@ +package makeus.cmc.malmo.domain.model.love_type; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; + +@Getter +@Builder(access = AccessLevel.PRIVATE) +public class LoveTypeMbtiPrompt { + private String mbti; + private LoveTypeCategory loveTypeCategory; + private String prompts; + + public static LoveTypeMbtiPrompt from(String mbti, LoveTypeCategory loveTypeCategory, String prompts) { + return LoveTypeMbtiPrompt.builder() + .mbti(normalizeMbti(mbti)) + .loveTypeCategory(loveTypeCategory) + .prompts(prompts) + .build(); + } + + private static String normalizeMbti(String mbti) { + return mbti == null ? null : mbti.toUpperCase(); + } +} diff --git a/src/test/java/makeus/cmc/malmo/application/service/chat/ChatPromptBuilderTest.java b/src/test/java/makeus/cmc/malmo/application/service/chat/ChatPromptBuilderTest.java new file mode 100644 index 00000000..909663c7 --- /dev/null +++ b/src/test/java/makeus/cmc/malmo/application/service/chat/ChatPromptBuilderTest.java @@ -0,0 +1,199 @@ +package makeus.cmc.malmo.application.service.chat; + +import makeus.cmc.malmo.application.helper.chat_room.ChatRoomQueryHelper; +import makeus.cmc.malmo.application.helper.chat_room.MemberChatRoomMetadataQueryHelper; +import makeus.cmc.malmo.application.helper.love_type.LoveTypeMbtiPromptQueryHelper; +import makeus.cmc.malmo.application.port.out.chat.LoadChatRoomMetadataPort; +import makeus.cmc.malmo.domain.model.chat.ChatRoom; +import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiPrompt; +import makeus.cmc.malmo.domain.model.member.Member; +import makeus.cmc.malmo.domain.value.id.InviteCodeValue; +import makeus.cmc.malmo.domain.value.type.EmailForwardingStatus; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import makeus.cmc.malmo.domain.value.type.MemberRole; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; +import makeus.cmc.malmo.domain.value.type.Provider; +import makeus.cmc.malmo.domain.value.type.RelationshipStatus; +import makeus.cmc.malmo.domain.value.state.MemberState; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ChatPromptBuilder 테스트") +class ChatPromptBuilderTest { + + private static final String UNKNOWN_INFERENCE_PROMPT = "UNKNOWN, 사용자와의 대화로부터 유추할 것"; + + @Mock + private ChatRoomQueryHelper chatRoomQueryHelper; + + @Mock + private MemberChatRoomMetadataQueryHelper memberChatRoomMetadataQueryHelper; + + @Mock + private LoveTypeMbtiPromptQueryHelper loveTypeMbtiPromptQueryHelper; + + @InjectMocks + private ChatPromptBuilder chatPromptBuilder; + + @Test + @DisplayName("사용자와 상대방 조합 프롬프트를 메타데이터에 삽입한다") + void createForProcessUserMessage_includesUserAndPartnerPrompts() { + // given + Member member = createMember("ISTJ", LoveTypeCategory.STABLE_TYPE, "ENFP", PartnerLoveTypeCategory.ANXIETY_TYPE); + ChatRoom chatRoom = createChatRoom(1L); + + stubCommon(chatRoom, LoveTypeCategory.STABLE_TYPE, PartnerLoveTypeCategory.ANXIETY_TYPE); + when(loveTypeMbtiPromptQueryHelper.findByMbtiAndLoveTypeCategory("ISTJ", LoveTypeCategory.STABLE_TYPE)) + .thenReturn(Optional.of(LoveTypeMbtiPrompt.from("ISTJ", LoveTypeCategory.STABLE_TYPE, "ISTJ 안정형 프롬프트"))); + when(loveTypeMbtiPromptQueryHelper.findByMbtiAndLoveTypeCategory("ENFP", LoveTypeCategory.ANXIETY_TYPE)) + .thenReturn(Optional.of(LoveTypeMbtiPrompt.from("ENFP", LoveTypeCategory.ANXIETY_TYPE, "ENFP 불안형 프롬프트"))); + + // when + List> messages = chatPromptBuilder.createForProcessUserMessage(member, chatRoom, "사용자 메시지"); + + // then + assertThat(messages).hasSize(2); + String metadata = messages.get(0).get("content"); + assertThat(metadata).contains("- 사용자 성향 프롬프트:"); + assertThat(metadata).contains("ISTJ 안정형 프롬프트"); + assertThat(metadata).contains("- 상대방 성향 프롬프트:"); + assertThat(metadata).contains("ENFP 불안형 프롬프트"); + } + + @Test + @DisplayName("사용자 정보가 없거나 상대방 애착유형이 UNKNOWN이면 폴백 문구를 삽입한다") + void createForProcessUserMessage_usesFallbackForUnknownCases() { + // given + Member member = createMember(null, null, "ENFP", PartnerLoveTypeCategory.UNKNOWN); + ChatRoom chatRoom = createChatRoom(2L); + + stubCommon(chatRoom, null, PartnerLoveTypeCategory.UNKNOWN); + + // when + List> messages = chatPromptBuilder.createForProcessUserMessage(member, chatRoom, "사용자 메시지"); + + // then + String metadata = messages.get(0).get("content"); + assertThat(metadata).contains("- 사용자 성향 프롬프트:\n" + UNKNOWN_INFERENCE_PROMPT); + assertThat(metadata).contains("- 상대방 성향 프롬프트:\n" + UNKNOWN_INFERENCE_PROMPT); + } + + @Test + @DisplayName("상대방 프로필이 없으면 상대방 성향 프롬프트 항목을 추가하지 않는다") + void createForProcessUserMessage_omitsPartnerPromptWithoutPartnerProfile() { + // given + Member member = createMember("ISTJ", LoveTypeCategory.STABLE_TYPE, null, null); + ChatRoom chatRoom = createChatRoom(3L); + + stubCommon(chatRoom, LoveTypeCategory.STABLE_TYPE, null); + when(loveTypeMbtiPromptQueryHelper.findByMbtiAndLoveTypeCategory("ISTJ", LoveTypeCategory.STABLE_TYPE)) + .thenReturn(Optional.of(LoveTypeMbtiPrompt.from("ISTJ", LoveTypeCategory.STABLE_TYPE, "ISTJ 안정형 프롬프트"))); + + // when + List> messages = chatPromptBuilder.createForProcessUserMessage(member, chatRoom, "사용자 메시지"); + + // then + String metadata = messages.get(0).get("content"); + assertThat(metadata).contains("- 사용자 성향 프롬프트:\nISTJ 안정형 프롬프트"); + assertThat(metadata).doesNotContain("- 상대방 성향 프롬프트:"); + } + + @Test + @DisplayName("조합 row가 없더라도 예외 없이 폴백 문구를 삽입한다") + void createForProcessUserMessage_fallsBackWhenPromptRowMissing() { + // given + Member member = createMember("ISTJ", LoveTypeCategory.STABLE_TYPE, "ENFP", PartnerLoveTypeCategory.ANXIETY_TYPE); + ChatRoom chatRoom = createChatRoom(4L); + + stubCommon(chatRoom, LoveTypeCategory.STABLE_TYPE, PartnerLoveTypeCategory.ANXIETY_TYPE); + when(loveTypeMbtiPromptQueryHelper.findByMbtiAndLoveTypeCategory(any(), any())) + .thenReturn(Optional.empty()); + + // when + List> messages = chatPromptBuilder.createForProcessUserMessage(member, chatRoom, "사용자 메시지"); + + // then + String metadata = messages.get(0).get("content"); + assertThat(metadata).contains("- 사용자 성향 프롬프트:\n" + UNKNOWN_INFERENCE_PROMPT); + assertThat(metadata).contains("- 상대방 성향 프롬프트:\n" + UNKNOWN_INFERENCE_PROMPT); + } + + private void stubCommon(ChatRoom chatRoom, LoveTypeCategory userLoveType, PartnerLoveTypeCategory partnerLoveType) { + when(chatRoomQueryHelper.getMemberMemoriesByMemberId(any())).thenReturn(List.of()); + when(chatRoomQueryHelper.getChatRoomMetadata(any())) + .thenReturn(new LoadChatRoomMetadataPort.ChatRoomMetadataDto(userLoveType, partnerLoveType)); + when(memberChatRoomMetadataQueryHelper.getMemberChatRoomMetadata(any())).thenReturn(List.of()); + when(chatRoomQueryHelper.getChatRoomLevelMessages(any(), any(Integer.class))).thenReturn(List.of()); + } + + private Member createMember( + String mbti, + LoveTypeCategory loveTypeCategory, + String partnerMbti, + PartnerLoveTypeCategory partnerLoveTypeCategory + ) { + LocalDateTime now = LocalDateTime.now(); + return Member.from( + 1L, + Provider.KAKAO, + "provider-id", + MemberRole.MEMBER, + MemberState.ALIVE, + false, + null, + null, + loveTypeCategory, + 1.0f, + 1.0f, + "tak", + "tak@example.com", + EmailForwardingStatus.ENABLED, + InviteCodeValue.of("ABCD1234"), + null, + null, + null, + RelationshipStatus.IN_RELATIONSHIP, + mbti, + partnerMbti, + partnerLoveTypeCategory, + now, + now, + null + ); + } + + private ChatRoom createChatRoom(Long id) { + LocalDateTime now = LocalDateTime.now(); + return ChatRoom.from( + id, + makeus.cmc.malmo.domain.value.id.MemberId.of(1L), + makeus.cmc.malmo.domain.value.state.ChatRoomState.ALIVE, + 1, + 1, + now, + null, + null, + null, + null, + null, + null, + now, + now, + null + ); + } +} diff --git a/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeMbtiPromptPersistenceAdapterTest.java b/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeMbtiPromptPersistenceAdapterTest.java new file mode 100644 index 00000000..ef1abb00 --- /dev/null +++ b/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeMbtiPromptPersistenceAdapterTest.java @@ -0,0 +1,50 @@ +package makeus.cmc.malmo.integration_test; + +import jakarta.persistence.EntityManager; +import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypeMbtiPromptEntity; +import makeus.cmc.malmo.application.port.out.LoadLoveTypeMbtiPromptPort; +import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiPrompt; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Transactional +@DisplayName("LoveTypeMbtiPromptPersistenceAdapter 테스트") +class LoveTypeMbtiPromptPersistenceAdapterTest { + + @Autowired + private EntityManager em; + + @Autowired + private LoadLoveTypeMbtiPromptPort loadLoveTypeMbtiPromptPort; + + @Test + @DisplayName("MBTI 대소문자와 복합키 기준으로 프롬프트를 조회한다") + void loadByMbtiAndLoveTypeCategory_findsPromptIgnoringMbtiCase() { + // given + em.persist(LoveTypeMbtiPromptEntity.builder() + .mbti("ISTJ") + .loveTypeCategory(LoveTypeCategory.STABLE_TYPE) + .prompts("ISTJ 안정형 프롬프트") + .build()); + em.flush(); + em.clear(); + + // when + LoveTypeMbtiPrompt prompt = loadLoveTypeMbtiPromptPort + .loadByMbtiAndLoveTypeCategory("istj", LoveTypeCategory.STABLE_TYPE) + .orElse(null); + + // then + assertThat(prompt).isNotNull(); + assertThat(prompt.getMbti()).isEqualTo("ISTJ"); + assertThat(prompt.getLoveTypeCategory()).isEqualTo(LoveTypeCategory.STABLE_TYPE); + assertThat(prompt.getPrompts()).isEqualTo("ISTJ 안정형 프롬프트"); + } +} From e96aff2a2757cd0d79120748a029926f1fb54767 Mon Sep 17 00:00:00 2001 From: Choi wontak <76509639+RoundTable02@users.noreply.github.com> Date: Mon, 16 Mar 2026 23:22:56 +0900 Subject: [PATCH 05/15] =?UTF-8?q?fix:=20mbti=20=EB=AA=85=EC=B9=AD=EC=9D=84?= =?UTF-8?q?=20personalityType=EC=9C=BC=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE.md} | 34 +-- docs/API-CHANGES-LOVETYPE-DATA.md | 26 +-- docs/API-CHANGES-LOVETYPE-MBTI-RESULT.md | 198 ------------------ ...HANGES-LOVETYPE-PERSONALITY-TYPE-RESULT.md | 51 +++++ ...OMPT-PERSONALITY-TYPE-LOVETYPE-EXAMPLE.md} | 18 +- sqls/MM-180.sql | 21 +- sqls/MM-181.sql | 16 +- .../malmo/adaptor/in/exception/ErrorCode.java | 2 +- .../in/exception/GlobalExceptionHandler.java | 8 +- .../in/web/controller/LoveTypeController.java | 23 +- .../in/web/controller/MemberController.java | 34 +-- .../adaptor/in/web/docs/SwaggerResponses.java | 28 +-- ...LoveTypeMbtiFeaturePersistenceAdapter.java | 25 --- .../LoveTypeMbtiPromptPersistenceAdapter.java | 25 --- ...sonalityTypeFeaturePersistenceAdapter.java | 29 +++ ...rsonalityTypePromptPersistenceAdapter.java | 29 +++ .../adapter/MemberPersistenceAdapter.java | 12 +- ...LoveTypePersonalityTypeFeatureEntity.java} | 28 +-- ... LoveTypePersonalityTypePromptEntity.java} | 12 +- .../entity/member/MemberEntity.java | 6 +- ...veTypePersonalityTypeFeatureEntityId.java} | 4 +- ...oveTypePersonalityTypePromptEntityId.java} | 4 +- .../mapper/LoveTypeMbtiPromptMapper.java | 21 -- ...LoveTypePersonalityTypeFeatureMapper.java} | 20 +- .../LoveTypePersonalityTypePromptMapper.java | 21 ++ .../out/persistence/mapper/MemberMapper.java | 8 +- .../LoveTypeMbtiFeatureRepository.java | 12 -- .../LoveTypeMbtiPromptRepository.java | 12 -- ...eTypePersonalityTypeFeatureRepository.java | 16 ++ ...veTypePersonalityTypePromptRepository.java | 16 ++ .../member/MemberRepositoryCustomImpl.java | 8 +- .../LoveTypeMbtiFeatureNotFoundException.java | 4 - ...rsonalityTypeFeatureNotFoundException.java | 4 + .../LoveTypeMbtiPromptQueryHelper.java | 20 -- ...eTypePersonalityTypePromptQueryHelper.java | 24 +++ .../helper/member/MemberQueryHelper.java | 6 +- ...LoveTypePersonalityTypeResultUseCase.java} | 20 +- .../member/CreatePartnerProfileUseCase.java | 4 +- .../port/in/member/GetMemberUseCase.java | 4 +- .../port/in/member/GetPartnerUseCase.java | 2 +- .../port/in/member/UpdateMemberUseCase.java | 4 +- .../member/UpdatePartnerProfileUseCase.java | 4 +- .../port/out/LoadLoveTypeMbtiFeaturePort.java | 10 - .../port/out/LoadLoveTypeMbtiPromptPort.java | 10 - ...oadLoveTypePersonalityTypeFeaturePort.java | 13 ++ ...LoadLoveTypePersonalityTypePromptPort.java | 13 ++ ...oveTypePersonalityTypeFeatureService.java} | 44 ++-- .../service/chat/ChatPromptBuilder.java | 32 +-- .../service/member/MemberCommandService.java | 12 +- .../service/member/MemberInfoService.java | 6 +- .../model/love_type/LoveTypeMbtiPrompt.java | 26 --- ...va => LoveTypePersonalityTypeFeature.java} | 40 ++-- .../LoveTypePersonalityTypePrompt.java | 30 +++ .../cmc/malmo/domain/model/member/Member.java | 34 +-- .../service/chat/ChatPromptBuilderTest.java | 28 +-- ...lityTypePromptPersistenceAdapterTest.java} | 26 +-- .../LoveTypeQuestionTest.java | 42 ++-- .../MemberIntegrationTest.java | 30 +-- .../cmc/malmo/mapper/MemberMapperTest.java | 12 +- .../service/AppleNotificationServiceTest.java | 4 +- 60 files changed, 572 insertions(+), 703 deletions(-) rename docs/{API-CHANGES-CHAT-PROMPT-MBTI-LOVETYPE.md => API-CHANGES-CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE.md} (72%) delete mode 100644 docs/API-CHANGES-LOVETYPE-MBTI-RESULT.md create mode 100644 docs/API-CHANGES-LOVETYPE-PERSONALITY-TYPE-RESULT.md rename docs/{CHAT-PROMPT-MBTI-LOVETYPE-EXAMPLE.md => CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE-EXAMPLE.md} (85%) delete mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiFeaturePersistenceAdapter.java delete mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiPromptPersistenceAdapter.java create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypePersonalityTypeFeaturePersistenceAdapter.java create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypePersonalityTypePromptPersistenceAdapter.java rename src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/{LoveTypeMbtiFeatureEntity.java => LoveTypePersonalityTypeFeatureEntity.java} (84%) rename src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/{LoveTypeMbtiPromptEntity.java => LoveTypePersonalityTypePromptEntity.java} (77%) rename src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/{LoveTypeMbtiPromptEntityId.java => LoveTypePersonalityTypeFeatureEntityId.java} (76%) rename src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/{LoveTypeMbtiFeatureEntityId.java => LoveTypePersonalityTypePromptEntityId.java} (76%) delete mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypeMbtiPromptMapper.java rename src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/{LoveTypeMbtiFeatureMapper.java => LoveTypePersonalityTypeFeatureMapper.java} (73%) create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypePersonalityTypePromptMapper.java delete mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiFeatureRepository.java delete mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiPromptRepository.java create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypePersonalityTypeFeatureRepository.java create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypePersonalityTypePromptRepository.java delete mode 100644 src/main/java/makeus/cmc/malmo/application/exception/LoveTypeMbtiFeatureNotFoundException.java create mode 100644 src/main/java/makeus/cmc/malmo/application/exception/LoveTypePersonalityTypeFeatureNotFoundException.java delete mode 100644 src/main/java/makeus/cmc/malmo/application/helper/love_type/LoveTypeMbtiPromptQueryHelper.java create mode 100644 src/main/java/makeus/cmc/malmo/application/helper/love_type/LoveTypePersonalityTypePromptQueryHelper.java rename src/main/java/makeus/cmc/malmo/application/port/in/{GetLoveTypeMbtiResultUseCase.java => GetLoveTypePersonalityTypeResultUseCase.java} (60%) delete mode 100644 src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiFeaturePort.java delete mode 100644 src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiPromptPort.java create mode 100644 src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypePersonalityTypeFeaturePort.java create mode 100644 src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypePersonalityTypePromptPort.java rename src/main/java/makeus/cmc/malmo/application/service/{LoveTypeMbtiFeatureService.java => LoveTypePersonalityTypeFeatureService.java} (61%) delete mode 100644 src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypeMbtiPrompt.java rename src/main/java/makeus/cmc/malmo/domain/model/love_type/{LoveTypeMbtiFeature.java => LoveTypePersonalityTypeFeature.java} (78%) create mode 100644 src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypePersonalityTypePrompt.java rename src/test/java/makeus/cmc/malmo/integration_test/{LoveTypeMbtiPromptPersistenceAdapterTest.java => LoveTypePersonalityTypePromptPersistenceAdapterTest.java} (50%) diff --git a/docs/API-CHANGES-CHAT-PROMPT-MBTI-LOVETYPE.md b/docs/API-CHANGES-CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE.md similarity index 72% rename from docs/API-CHANGES-CHAT-PROMPT-MBTI-LOVETYPE.md rename to docs/API-CHANGES-CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE.md index 89c4c3a9..6409df1a 100644 --- a/docs/API-CHANGES-CHAT-PROMPT-MBTI-LOVETYPE.md +++ b/docs/API-CHANGES-CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE.md @@ -1,8 +1,8 @@ -# 채팅 프롬프트 변경 사항 - MBTI + 애착유형 조합 프롬프트 주입 +# 채팅 프롬프트 변경 사항 - personalityType + 애착유형 조합 프롬프트 주입 ## 개요 -채팅 시스템 메시지의 `[사용자 메타데이터]` 블록에 사용자와 상대방의 `MBTI + 애착유형` 조합별 프롬프트를 함께 주입합니다. +채팅 시스템 메시지의 `[사용자 메타데이터]` 블록에 사용자와 상대방의 `personalityType + 애착유형` 조합별 프롬프트를 함께 주입합니다. 이 변경의 목적은 다음과 같습니다. @@ -16,28 +16,28 @@ ### 신규 테이블 -`love_type_mbti_prompt` +`love_type_personality_type_prompt` | column | type | description | | --- | --- | --- | -| `mbti` | `VARCHAR(4)` | MBTI 4자리 문자열 | +| `personality_type` | `VARCHAR(4)` | MBTI 4자리 문자열 | | `lovetype` | `VARCHAR(255)` | `LoveTypeCategory` enum 문자열 | | `prompts` | `TEXT` | 채팅 메타데이터에 삽입할 프롬프트 전문 | ### 키 규칙 -- `(mbti, lovetype)` 복합 PK +- `(personality_type, lovetype)` 복합 PK - 애플리케이션은 MBTI를 대문자로 정규화해 조회합니다. - `UNKNOWN` row는 저장하지 않습니다. ### DDL ```sql -CREATE TABLE love_type_mbti_prompt ( - mbti VARCHAR(4) NOT NULL, +CREATE TABLE love_type_personality_type_prompt ( + personality_type VARCHAR(4) NOT NULL, lovetype VARCHAR(255) NOT NULL, prompts TEXT, - PRIMARY KEY (mbti, lovetype) + PRIMARY KEY (personality_type, lovetype) ); ``` @@ -61,7 +61,7 @@ CREATE TABLE love_type_mbti_prompt ( ### 사용자 본인 -- `member.mbti`와 `member.loveTypeCategory`가 모두 있으면 `love_type_mbti_prompt`를 조회합니다. +- `member.personalityType`와 `member.loveTypeCategory`가 모두 있으면 `love_type_personality_type_prompt`를 조회합니다. - 조합 row가 존재하면 `prompts` 컬럼 값을 그대로 삽입합니다. - 둘 중 하나라도 없거나 조합 row가 없으면 아래 폴백 문구를 삽입합니다. @@ -71,9 +71,9 @@ UNKNOWN, 사용자와의 대화로부터 유추할 것 ### 상대방 -- `partnerMbti`가 있는 경우에만 `- 상대방 성향 프롬프트:` 항목을 추가합니다. +- `otherPersonalityType`가 있는 경우에만 `- 상대방 성향 프롬프트:` 항목을 추가합니다. - `partnerLoveTypeCategory`가 `UNKNOWN` 또는 `null`이면 DB 조회 없이 바로 폴백 문구를 삽입합니다. -- `partnerMbti`와 확정된 `partnerLoveTypeCategory`가 모두 있으면 `(partnerMbti, partnerLoveTypeCategory)` 조합으로 조회합니다. +- `otherPersonalityType`와 확정된 `partnerLoveTypeCategory`가 모두 있으면 `(otherPersonalityType, partnerLoveTypeCategory)` 조합으로 조회합니다. - 조합 row가 없으면 채팅은 실패시키지 않고 동일한 폴백 문구를 삽입합니다. --- @@ -89,9 +89,9 @@ UNKNOWN, 사용자와의 대화로부터 유추할 것 아래 경우에는 모두 동일한 폴백 문구를 사용합니다. -- 사용자 MBTI 없음 +- 사용자 personalityType 없음 - 사용자 애착유형 없음 -- 상대방 MBTI 없음 +- 상대방 personalityType 없음 - 상대방 애착유형이 `UNKNOWN` - 상대방 애착유형이 `null` - 조합 row 없음 @@ -110,9 +110,9 @@ UNKNOWN, 사용자와의 대화로부터 유추할 것 다만 아래 프로필 데이터가 채팅 프롬프트 생성에 직접 활용됩니다. -- `GET /members`의 `mbti` +- `GET /members`의 `personalityType` - `GET /members`의 `loveTypeCategory` -- `GET /members`의 `partnerMbti` +- `GET /members`의 `otherPersonalityType` - `GET /members`의 `partnerLoveTypeCategory` --- @@ -132,6 +132,6 @@ UNKNOWN, 사용자와의 대화로부터 유추할 것 관련 테스트: - `src/test/java/makeus/cmc/malmo/application/service/chat/ChatPromptBuilderTest.java` -- `src/test/java/makeus/cmc/malmo/integration_test/LoveTypeMbtiPromptPersistenceAdapterTest.java` +- `src/test/java/makeus/cmc/malmo/integration_test/LoveTypePersonalityTypePromptPersistenceAdapterTest.java` -실제 시스템 메시지 예시는 `docs/CHAT-PROMPT-MBTI-LOVETYPE-EXAMPLE.md`를 참고합니다. +실제 시스템 메시지 예시는 `docs/CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE-EXAMPLE.md`를 참고합니다. diff --git a/docs/API-CHANGES-LOVETYPE-DATA.md b/docs/API-CHANGES-LOVETYPE-DATA.md index 1a439930..71ec1d05 100644 --- a/docs/API-CHANGES-LOVETYPE-DATA.md +++ b/docs/API-CHANGES-LOVETYPE-DATA.md @@ -3,7 +3,7 @@ **Request** ```ts { - mbti: string, + personalityType: string, loveTypeCategory: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE' | null // null = "모르겠어요" 선택 } @@ -11,7 +11,7 @@ **Response** ```ts { - mbti: string, + personalityType: string, loveTypeCategory: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE' | 'UNKNOWN', description: string } @@ -21,14 +21,14 @@ **Request** ```ts { - mbti?: string, + personalityType?: string, loveTypeCategory?: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE' | null } ``` **Response** ```ts { - mbti: string, + personalityType: string, loveTypeCategory: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE' | 'UNKNOWN', description: string } @@ -46,9 +46,9 @@ 기존 응답 유지, 아래 필드 추가 ```ts { - mbti: string, // 내 MBTI + personalityType: string, // 내 MBTI loveTypeCategory: enum, // 내 애착유형 - partnerMbti: string, // 상대 MBTI + otherPersonalityType: string, // 상대 MBTI partnerLoveTypeCategory: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE' | 'UNKNOWN' // undefined = 미입력 / UNKNOWN = "모르겠어요" 선택됨 } @@ -58,24 +58,24 @@ ## 채팅 프롬프트 활용 -사용자와 상대방 프로필의 `mbti`, `loveTypeCategory`, `partnerMbti`, `partnerLoveTypeCategory`는 채팅 시스템 메시지의 메타데이터 구성에도 사용됩니다. +사용자와 상대방 프로필의 `personalityType`, `loveTypeCategory`, `otherPersonalityType`, `partnerLoveTypeCategory`는 채팅 시스템 메시지의 메타데이터 구성에도 사용됩니다. ### 활용 규칙 - 사용자 본인 - - `mbti`와 `loveTypeCategory`가 모두 있으면 `(mbti, lovetype)` 조합으로 상세 프롬프트를 조회합니다. + - `personalityType`와 `loveTypeCategory`가 모두 있으면 `(personalityType, lovetype)` 조합으로 상세 프롬프트를 조회합니다. - 둘 중 하나라도 없거나 매칭 row가 없으면 `UNKNOWN, 사용자와의 대화로부터 유추할 것`을 사용합니다. - 상대방 - - `partnerMbti`가 있을 때만 상대방 성향 프롬프트 항목이 추가됩니다. + - `otherPersonalityType`가 있을 때만 상대방 성향 프롬프트 항목이 추가됩니다. - `partnerLoveTypeCategory`가 `UNKNOWN` 또는 `null`이면 DB 조회 없이 `UNKNOWN, 사용자와의 대화로부터 유추할 것`을 사용합니다. - - `partnerMbti`와 확정된 `partnerLoveTypeCategory`가 모두 있으면 `(partnerMbti, partnerLoveTypeCategory)` 조합으로 상세 프롬프트를 조회합니다. + - `otherPersonalityType`와 확정된 `partnerLoveTypeCategory`가 모두 있으면 `(otherPersonalityType, partnerLoveTypeCategory)` 조합으로 상세 프롬프트를 조회합니다. - 매칭 row가 없으면 채팅은 실패하지 않고 동일한 폴백 문구를 사용합니다. ### 조회 대상 테이블 -- `love_type_mbti_prompt` - - `mbti`: MBTI 4자리 문자열 +- `love_type_personality_type_prompt` + - `personality_type`: MBTI 4자리 문자열 - `lovetype`: `STABLE_TYPE | ANXIETY_TYPE | AVOIDANCE_TYPE | CONFUSION_TYPE` - `prompts`: 실제 채팅 메타데이터에 삽입할 프롬프트 전문 -상세 동작 예시는 `docs/API-CHANGES-CHAT-PROMPT-MBTI-LOVETYPE.md`, 실제 전달 예시는 `docs/CHAT-PROMPT-MBTI-LOVETYPE-EXAMPLE.md`를 참고합니다. +상세 동작 예시는 `docs/API-CHANGES-CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE.md`, 실제 전달 예시는 `docs/CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE-EXAMPLE.md`를 참고합니다. diff --git a/docs/API-CHANGES-LOVETYPE-MBTI-RESULT.md b/docs/API-CHANGES-LOVETYPE-MBTI-RESULT.md deleted file mode 100644 index ebccaca2..00000000 --- a/docs/API-CHANGES-LOVETYPE-MBTI-RESULT.md +++ /dev/null @@ -1,198 +0,0 @@ -## 신규 API 스펙 - -### `GET /love-types/result` - MBTI + 애착유형 상세 결과 조회 - -`mbti`와 `lovetype` 조합으로 MBTI별 애착유형 상세 결과를 조회합니다. - -기존 API와의 차이: -- `POST /love-types/result`: 검사 답변을 제출하고 임시 결과를 생성 -- `GET /love-types/result/{loveTypeId}`: 생성된 임시 검사 결과를 조회 -- `GET /love-types/result`: `(mbti, lovetype)` 조합에 대응하는 고정 상세 콘텐츠를 조회 - ---- - -## Request - -### Query Parameters - -| name | type | required | description | -| --- | --- | --- | --- | -| `mbti` | String | Y | 영문 4자리 MBTI. 대소문자 무관, 내부적으로 대문자로 정규화 | -| `lovetype` | String | Y | `STABLE_TYPE`, `ANXIETY_TYPE`, `AVOIDANCE_TYPE`, `CONFUSION_TYPE` 중 하나. 대소문자 무관 | - -### Example - -```http -GET /love-types/result?mbti=enfp&lovetype=stable_type -``` - ---- - -## Response - -```ts -{ - mbti: string, - loveTypeCategory: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE', - summary: string, - keywords: string[], - strengths: Array<{ - title: string | null, - description: string | null - }>, - weaknesses: Array<{ - title: string | null, - description: string | null - }>, - patterns: Array<{ - title: string | null, - description: string | null - }>, - loveTypeFeatures: Array<{ - title: string | null, - description: string | null - }>, - datingGuides: string[], - bestMatches: Array<{ - mbti: string | null, - description: string | null - }>, - worstMatches: Array<{ - mbti: string | null, - description: string | null - }> -} -``` - -### Example Response - -```json -{ - "mbti": "ENFP", - "loveTypeCategory": "STABLE_TYPE", - "summary": "풍부한 상상력과 사랑으로, 함께하는 일상을 즐겁게 만들어 가는 유형", - "keywords": ["열정적", "자유로움", "생기발랄"], - "strengths": [ - { - "title": "Ne", - "description": "흩어진 정보 속에서 하나의 핵심 맥락과 미래를 읽어내요" - }, - { - "title": "Fi", - "description": "나의 진솔한 감정을 공유하는 것을 최고의 가치로 여겨요" - } - ], - "weaknesses": [ - { - "title": "Si", - "description": "반복되는 루틴에 답답함을 느껴요" - } - ], - "patterns": [ - { - "title": "나다운 기준을 지켜요", - "description": "여러 가능성 속에서 무엇이 나에게 의미 있는지 먼저 생각해요" - } - ], - "loveTypeFeatures": [ - { - "title": "미래의 가능성을 자주 상상해요", - "description": "연인과 함께할 미래의 가능성을 상상하며 창의적인 질문으로 관계를 만들어요" - } - ], - "datingGuides": ["감정을 정리해 표현해요"], - "bestMatches": [ - { - "mbti": "INFJ", - "description": "속마음을 깊이 이해해주며 안정적인 감정을 공유하는 궁합" - } - ], - "worstMatches": [ - { - "mbti": "ISTP", - "description": "자유로운 감정선과 솔직한 피드백이 부딪히는 궁합" - } - ] -} -``` - ---- - -## DB 매핑 기준 - -조회 대상 테이블은 `love_type_mbti_feature`입니다. - -테이블 컬럼은 `docs/mbti-lovetype-feature.md` 헤더를 기준으로 그대로 맞춥니다. - -| column | description | -| --- | --- | -| `lovetype` | 애착유형 enum 문자열 | -| `mbti` | MBTI 4자리 문자열 | -| `summary` | 요약 문구 | -| `keyword1` ~ `keyword3` | 키워드 | -| `strength1` ~ `strength3` | 강점 제목 | -| `strength_desc1` ~ `strength_desc3` | 강점 설명 | -| `weakness` | 약점 제목 | -| `weakness_desc` | 약점 설명 | -| `pattern_title1` ~ `pattern_title4` | 관계 패턴 제목 | -| `pattern1` ~ `pattern4` | 관계 패턴 설명 | -| `lovetype_feature_title1` ~ `lovetype_feature_title4` | 애착유형 특징 제목 | -| `lovetype_feature1` ~ `lovetype_feature4` | 애착유형 특징 설명 | -| `dating_guide1` ~ `dating_guide3` | 연애 가이드 | -| `best_mbti1` ~ `best_mbti2` | 잘 맞는 MBTI | -| `best_desc1` ~ `best_desc2` | 잘 맞는 MBTI 설명 | -| `worst_mbti1` ~ `worst_mbti2` | 부딪히기 쉬운 MBTI | -| `worst_desc1` ~ `worst_desc2` | 부딪히기 쉬운 MBTI 설명 | - -주의: -- 애플리케이션은 별도 audit 컬럼 없이 위 문서 컬럼만 기준으로 조회합니다. -- `(mbti, lovetype)` 조합은 유니크하다고 가정합니다. - ---- - -## 응답 가공 규칙 - -문서의 번호형 컬럼은 응답에서 묶어서 반환합니다. - -- `keyword1` ~ `keyword3` -> `keywords[]` -- `strengthN + strength_descN` -> `strengths[]` -- `weakness + weakness_desc` -> `weaknesses[]` -- `pattern_titleN + patternN` -> `patterns[]` -- `lovetype_feature_titleN + lovetype_featureN` -> `loveTypeFeatures[]` -- `dating_guideN` -> `datingGuides[]` -- `best_mbtiN + best_descN` -> `bestMatches[]` -- `worst_mbtiN + worst_descN` -> `worstMatches[]` - -빈 문자열 또는 null은 응답 배열에서 제외합니다. - -예: -- `strength2=""`, `strength_desc2=""` 이면 `strengths` 배열에 두 번째 항목은 포함되지 않습니다. -- `best_mbti2`만 비어 있어도 `bestMatches` 두 번째 항목은 제외됩니다. - ---- - -## 에러 응답 - -### 잘못된 요청 -- `mbti`가 영문 4자리가 아닌 경우 -- `lovetype`이 허용 enum 값이 아닌 경우 -- 필수 query parameter가 누락된 경우 - -```json -{ - "success": false, - "message": "잘못된 요청입니다.", - "code": 40000 -} -``` - -### 데이터 없음 -- `mbti + lovetype` 조합에 해당하는 row가 없는 경우 - -```json -{ - "success": false, - "message": "해당 MBTI와 애착 유형 결과가 존재하지 않습니다.", - "code": 40019 -} -``` diff --git a/docs/API-CHANGES-LOVETYPE-PERSONALITY-TYPE-RESULT.md b/docs/API-CHANGES-LOVETYPE-PERSONALITY-TYPE-RESULT.md new file mode 100644 index 00000000..926de09b --- /dev/null +++ b/docs/API-CHANGES-LOVETYPE-PERSONALITY-TYPE-RESULT.md @@ -0,0 +1,51 @@ +## 신규 API 스펙 + +### `GET /love-types/result` - personalityType + 애착유형 상세 결과 조회 + +`personalityType`와 `lovetype` 조합으로 상세 결과를 조회합니다. + +기존 API와의 차이: +- `POST /love-types/result`: 검사 답변을 제출하고 임시 결과를 생성 +- `GET /love-types/result/{loveTypeId}`: 생성된 임시 검사 결과를 조회 +- `GET /love-types/result`: `(personalityType, lovetype)` 조합에 대응하는 고정 상세 콘텐츠를 조회 + +## Request + +| name | type | required | description | +| --- | --- | --- | --- | +| `personalityType` | String | Y | 영문 4자리 MBTI. 대소문자 무관, 내부적으로 대문자로 정규화 | +| `lovetype` | String | Y | `STABLE_TYPE`, `ANXIETY_TYPE`, `AVOIDANCE_TYPE`, `CONFUSION_TYPE` 중 하나 | + +```http +GET /love-types/result?personalityType=enfp&lovetype=stable_type +``` + +## Response + +```ts +{ + personalityType: string, + loveTypeCategory: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE', + summary: string, + keywords: string[], + strengths: Array<{ title: string | null, description: string | null }>, + weaknesses: Array<{ title: string | null, description: string | null }>, + patterns: Array<{ title: string | null, description: string | null }>, + loveTypeFeatures: Array<{ title: string | null, description: string | null }>, + datingGuides: string[], + bestMatches: Array<{ personalityType: string | null, description: string | null }>, + worstMatches: Array<{ personalityType: string | null, description: string | null }> +} +``` + +## DB 매핑 기준 + +- 조회 대상 테이블은 `love_type_personality_type_feature`입니다. +- 복합키는 `(personality_type, lovetype)`입니다. +- 궁합 컬럼은 `best_personality_type1/2`, `worst_personality_type1/2`를 사용합니다. + +## 에러 응답 + +- `personalityType`가 영문 4자리가 아닌 경우 +- `lovetype`이 허용 enum 값이 아닌 경우 +- `personalityType + lovetype` 조합에 해당하는 row가 없는 경우 diff --git a/docs/CHAT-PROMPT-MBTI-LOVETYPE-EXAMPLE.md b/docs/CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE-EXAMPLE.md similarity index 85% rename from docs/CHAT-PROMPT-MBTI-LOVETYPE-EXAMPLE.md rename to docs/CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE-EXAMPLE.md index 1d55716d..6f02de51 100644 --- a/docs/CHAT-PROMPT-MBTI-LOVETYPE-EXAMPLE.md +++ b/docs/CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE-EXAMPLE.md @@ -1,4 +1,4 @@ -# 채팅 프롬프트 예시 - MBTI + 애착유형 메타데이터 주입 +# 채팅 프롬프트 예시 - personalityType + 애착유형 메타데이터 주입 ## 목적 @@ -15,11 +15,11 @@ 가정: -- 사용자 MBTI: `ISTJ` +- 사용자 personalityType: `ISTJ` - 사용자 애착유형: `STABLE_TYPE` -- 상대방 MBTI: `ENFP` +- 상대방 personalityType: `ENFP` - 상대방 애착유형: `ANXIETY_TYPE` -- 두 조합 모두 `love_type_mbti_prompt` 테이블에 row 존재 +- 두 조합 모두 `love_type_personality_type_prompt` 테이블에 row 존재 실제로 모델에 들어가는 첫 system 메시지 예시: @@ -52,9 +52,9 @@ ENFP 불안형 가정: -- 사용자 MBTI: `ISTJ` +- 사용자 personalityType: `ISTJ` - 사용자 애착유형: `STABLE_TYPE` -- 상대방 MBTI: `ENFP` +- 상대방 personalityType: `ENFP` - 상대방 애착유형: `UNKNOWN` ```text @@ -78,7 +78,7 @@ UNKNOWN, 사용자와의 대화로부터 유추할 것 핵심: -- `partnerMbti`가 있으므로 상대방 성향 프롬프트 항목은 생성됩니다. +- `otherPersonalityType`가 있으므로 상대방 성향 프롬프트 항목은 생성됩니다. - `partnerLoveTypeCategory == UNKNOWN` 이므로 DB 조회 없이 폴백 문구가 들어갑니다. --- @@ -87,9 +87,9 @@ UNKNOWN, 사용자와의 대화로부터 유추할 것 가정: -- 사용자 MBTI: `INFP` +- 사용자 personalityType: `INFP` - 사용자 애착유형: `CONFUSION_TYPE` -- `love_type_mbti_prompt`에 `(INFP, CONFUSION_TYPE)` row 없음 +- `love_type_personality_type_prompt`에 `(INFP, CONFUSION_TYPE)` row 없음 ```text [사용자 메타데이터] diff --git a/sqls/MM-180.sql b/sqls/MM-180.sql index 5ae72899..54975a5d 100644 --- a/sqls/MM-180.sql +++ b/sqls/MM-180.sql @@ -1,26 +1,7 @@ --- MM-180: Add direct partner profile fields to member_entity +-- MM-180: Add direct partner love type field to member_entity -- Author: Codex -- Date: 2026-03-16 -ALTER TABLE member_entity -ADD COLUMN mbti VARCHAR(4); - -ALTER TABLE member_entity -ADD COLUMN partner_mbti VARCHAR(4); - ALTER TABLE member_entity ADD COLUMN partner_love_type_category VARCHAR(255); - -UPDATE member_entity -SET mbti = UPPER(personality_type) -WHERE personality_type IS NOT NULL - AND mbti IS NULL; - -UPDATE member_entity -SET partner_mbti = UPPER(other_personality_type) -WHERE other_personality_type IS NOT NULL - AND partner_mbti IS NULL; - -COMMENT ON COLUMN member_entity.mbti IS 'Member MBTI'; -COMMENT ON COLUMN member_entity.partner_mbti IS 'Partner MBTI entered directly by member'; COMMENT ON COLUMN member_entity.partner_love_type_category IS 'Partner love type category: STABLE_TYPE, ANXIETY_TYPE, AVOIDANCE_TYPE, CONFUSION_TYPE, UNKNOWN'; diff --git a/sqls/MM-181.sql b/sqls/MM-181.sql index 1c3463cd..092745d0 100644 --- a/sqls/MM-181.sql +++ b/sqls/MM-181.sql @@ -1,15 +1,15 @@ --- MM-181: Add love_type_mbti_prompt table for chat prompt enrichment +-- MM-181: Add love_type_personality_type_prompt table for chat prompt enrichment -- Author: Codex -- Date: 2026-03-16 -CREATE TABLE love_type_mbti_prompt ( - mbti VARCHAR(4) NOT NULL, +CREATE TABLE love_type_personality_type_prompt ( + personality_type VARCHAR(4) NOT NULL, lovetype VARCHAR(255) NOT NULL, prompts TEXT, - PRIMARY KEY (mbti, lovetype) + PRIMARY KEY (personality_type, lovetype) ); -COMMENT ON TABLE love_type_mbti_prompt IS 'MBTI and love type specific prompt snippets for chat metadata'; -COMMENT ON COLUMN love_type_mbti_prompt.mbti IS 'MBTI in 4-letter uppercase format'; -COMMENT ON COLUMN love_type_mbti_prompt.lovetype IS 'Love type category: STABLE_TYPE, ANXIETY_TYPE, AVOIDANCE_TYPE, CONFUSION_TYPE'; -COMMENT ON COLUMN love_type_mbti_prompt.prompts IS 'Prompt content to inject into chat metadata'; +COMMENT ON TABLE love_type_personality_type_prompt IS 'Personality type and love type specific prompt snippets for chat metadata'; +COMMENT ON COLUMN love_type_personality_type_prompt.personality_type IS 'MBTI in 4-letter uppercase format'; +COMMENT ON COLUMN love_type_personality_type_prompt.lovetype IS 'Love type category: STABLE_TYPE, ANXIETY_TYPE, AVOIDANCE_TYPE, CONFUSION_TYPE'; +COMMENT ON COLUMN love_type_personality_type_prompt.prompts IS 'Prompt content to inject into chat metadata'; diff --git a/src/main/java/makeus/cmc/malmo/adaptor/in/exception/ErrorCode.java b/src/main/java/makeus/cmc/malmo/adaptor/in/exception/ErrorCode.java index c11e6479..3691390e 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/exception/ErrorCode.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/in/exception/ErrorCode.java @@ -28,7 +28,7 @@ public enum ErrorCode { NO_SUCH_MESSAGE(HttpStatus.BAD_REQUEST, 40016, "메시지가 존재하지 않습니다."), PARTNER_PROFILE_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, 40017, "이미 상대 프로필이 등록되어 있습니다."), NO_SUCH_PARTNER_PROFILE(HttpStatus.BAD_REQUEST, 40018, "등록된 상대 프로필이 존재하지 않습니다."), - NO_SUCH_LOVE_TYPE_MBTI_RESULT(HttpStatus.BAD_REQUEST, 40019, "해당 MBTI와 애착 유형 결과가 존재하지 않습니다."), + NO_SUCH_LOVE_TYPE_PERSONALITY_TYPE_RESULT(HttpStatus.BAD_REQUEST, 40019, "해당 MBTI와 애착 유형 결과가 존재하지 않습니다."), // 401 Unauthorized UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 40100, "인증되지 않은 사용자입니다."), 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 c0ccbca8..5a55397e 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 @@ -182,10 +182,10 @@ public ResponseEntity handlePartnerProfileNotFoundException(Partn return ErrorResponse.of(ErrorCode.NO_SUCH_PARTNER_PROFILE); } - @ExceptionHandler({LoveTypeMbtiFeatureNotFoundException.class}) - public ResponseEntity handleLoveTypeMbtiFeatureNotFoundException(LoveTypeMbtiFeatureNotFoundException e) { - log.warn("[GlobalExceptionHandler: handleLoveTypeMbtiFeatureNotFoundException 호출] {}", e.getMessage()); - return ErrorResponse.of(ErrorCode.NO_SUCH_LOVE_TYPE_MBTI_RESULT); + @ExceptionHandler({LoveTypePersonalityTypeFeatureNotFoundException.class}) + public ResponseEntity handleLoveTypePersonalityTypeFeatureNotFoundException(LoveTypePersonalityTypeFeatureNotFoundException e) { + log.warn("[GlobalExceptionHandler: handleLoveTypePersonalityTypeFeatureNotFoundException 호출] {}", e.getMessage()); + return ErrorResponse.of(ErrorCode.NO_SUCH_LOVE_TYPE_PERSONALITY_TYPE_RESULT); } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/LoveTypeController.java b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/LoveTypeController.java index f5724302..91f0a85d 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/LoveTypeController.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/LoveTypeController.java @@ -19,7 +19,7 @@ import makeus.cmc.malmo.adaptor.in.web.dto.BaseListResponse; import makeus.cmc.malmo.adaptor.in.web.dto.BaseResponse; import makeus.cmc.malmo.application.port.in.CalculateQuestionResultUseCase; -import makeus.cmc.malmo.application.port.in.GetLoveTypeMbtiResultUseCase; +import makeus.cmc.malmo.application.port.in.GetLoveTypePersonalityTypeResultUseCase; import makeus.cmc.malmo.application.port.in.GetLoveTypeQuestionResultUseCase; import makeus.cmc.malmo.application.port.in.GetLoveTypeQuestionsUseCase; import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; @@ -41,7 +41,7 @@ public class LoveTypeController { private final GetLoveTypeQuestionsUseCase getLoveTypeQuestionsUseCase; private final CalculateQuestionResultUseCase calculateQuestionResultUseCase; private final GetLoveTypeQuestionResultUseCase getLoveTypeQuestionResultUseCase; - private final GetLoveTypeMbtiResultUseCase getLoveTypeMbtiResultUseCase; + private final GetLoveTypePersonalityTypeResultUseCase getLoveTypePersonalityTypeResultUseCase; @Operation( summary = "애착 유형 검사 질문 조회", @@ -139,7 +139,7 @@ public BaseResponse get @ApiResponse( responseCode = "200", description = "애착 유형 상세 결과 조회 성공", - content = @Content(schema = @Schema(implementation = SwaggerResponses.LoveTypeMbtiResultSuccessResponse.class)) + content = @Content(schema = @Schema(implementation = SwaggerResponses.LoveTypePersonalityTypeResultSuccessResponse.class)) ) @ApiResponses(value = { @ApiResponse( @@ -154,10 +154,10 @@ public BaseResponse get ) }) @GetMapping("/result") - public BaseResponse getLoveTypeMbtiResult( - @RequestParam + public BaseResponse getLoveTypePersonalityTypeResult( + @RequestParam("personalityType") @Pattern(regexp = "^[a-zA-Z]{4}$", message = "MBTI는 영문 4자리여야 합니다.") - String mbti, + String personalityType, @RequestParam("lovetype") @Pattern( regexp = "(?i)^(STABLE_TYPE|ANXIETY_TYPE|AVOIDANCE_TYPE|CONFUSION_TYPE)$", @@ -165,12 +165,13 @@ public BaseResponse get ) String loveType ) { - GetLoveTypeMbtiResultUseCase.GetLoveTypeMbtiResultCommand command = GetLoveTypeMbtiResultUseCase.GetLoveTypeMbtiResultCommand.builder() - .mbti(normalizeMbti(mbti)) + GetLoveTypePersonalityTypeResultUseCase.GetLoveTypePersonalityTypeResultCommand command = + GetLoveTypePersonalityTypeResultUseCase.GetLoveTypePersonalityTypeResultCommand.builder() + .personalityType(normalizePersonalityType(personalityType)) .loveTypeCategory(normalizeLoveTypeCategory(loveType)) .build(); - return BaseResponse.success(getLoveTypeMbtiResultUseCase.getResult(command)); + return BaseResponse.success(getLoveTypePersonalityTypeResultUseCase.getResult(command)); } @Data @@ -188,8 +189,8 @@ public static class LoveTypeTestResult { private Integer score; } - private static String normalizeMbti(String mbti) { - return mbti == null ? null : mbti.toUpperCase(Locale.ROOT); + private static String normalizePersonalityType(String personalityType) { + return personalityType == null ? null : personalityType.toUpperCase(Locale.ROOT); } private static LoveTypeCategory normalizeLoveTypeCategory(String loveType) { 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 c48d0ccb..5f5fdb83 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 @@ -108,7 +108,7 @@ public BaseResponse updateMember( .memberId(Long.valueOf(user.getUsername())) .nickname(requestDto.getNickname()) .relationshipStatus(requestDto.getRelationshipStatus()) - .mbti(normalizeMbti(requestDto.getMbti())) + .personalityType(normalizePersonalityType(requestDto.getPersonalityType())) .loveTypeCategory(requestDto.getLoveTypeCategory()) .build(); return BaseResponse.success(updateMemberUseCase.updateMember(command)); @@ -133,7 +133,7 @@ public BaseResponse creat CreatePartnerProfileUseCase.CreatePartnerProfileCommand command = CreatePartnerProfileUseCase.CreatePartnerProfileCommand.builder() .memberId(Long.valueOf(user.getUsername())) - .mbti(normalizeMbti(requestDto.getMbti())) + .personalityType(normalizePersonalityType(requestDto.getPersonalityType())) .loveTypeCategory(requestDto.getLoveTypeCategory()) .build(); @@ -159,8 +159,8 @@ public BaseResponse updat UpdatePartnerProfileUseCase.UpdatePartnerProfileCommand command = UpdatePartnerProfileUseCase.UpdatePartnerProfileCommand.builder() .memberId(Long.valueOf(user.getUsername())) - .mbti(normalizeMbti(requestDto.getMbti())) - .mbtiProvided(requestDto.isMbtiProvided()) + .personalityType(normalizePersonalityType(requestDto.getPersonalityType())) + .personalityTypeProvided(requestDto.isPersonalityTypeProvided()) .loveTypeCategory(requestDto.getLoveTypeCategory()) .loveTypeCategoryProvided(requestDto.isLoveTypeCategoryProvided()) .build(); @@ -316,7 +316,7 @@ public static class UpdateMemberRequestDto { private RelationshipStatus relationshipStatus; @Pattern(regexp = "^[a-zA-Z]{4}$", message = "MBTI는 영문 4자리여야 합니다.") - private String mbti; + private String personalityType; private LoveTypeCategory loveTypeCategory; } @@ -325,24 +325,24 @@ public static class UpdateMemberRequestDto { public static class CreatePartnerProfileRequestDto { @NotBlank(message = "상대 MBTI는 필수 입력값입니다.") @Pattern(regexp = "^[a-zA-Z]{4}$", message = "MBTI는 영문 4자리여야 합니다.") - private String mbti; + private String personalityType; private PartnerLoveTypeCategory loveTypeCategory; } public static class UpdatePartnerProfileRequestDto { @Pattern(regexp = "^[a-zA-Z]{4}$", message = "MBTI는 영문 4자리여야 합니다.") - private String mbti; - private boolean mbtiProvided; + private String personalityType; + private boolean personalityTypeProvided; private PartnerLoveTypeCategory loveTypeCategory; private boolean loveTypeCategoryProvided; - public String getMbti() { - return mbti; + public String getPersonalityType() { + return personalityType; } - public boolean isMbtiProvided() { - return mbtiProvided; + public boolean isPersonalityTypeProvided() { + return personalityTypeProvided; } public PartnerLoveTypeCategory getLoveTypeCategory() { @@ -353,9 +353,9 @@ public boolean isLoveTypeCategoryProvided() { return loveTypeCategoryProvided; } - public void setMbti(String mbti) { - this.mbti = mbti; - this.mbtiProvided = true; + public void setPersonalityType(String personalityType) { + this.personalityType = personalityType; + this.personalityTypeProvided = true; } public void setLoveTypeCategory(PartnerLoveTypeCategory loveTypeCategory) { @@ -401,8 +401,8 @@ public static class LoveTypeTestResult { private Integer score; } - private static String normalizeMbti(String mbti) { - return mbti == null ? null : mbti.toUpperCase(); + private static String normalizePersonalityType(String personalityType) { + return personalityType == null ? null : personalityType.toUpperCase(); } } 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 e1ba8684..0dadb1e5 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 @@ -145,7 +145,7 @@ public static class LoveTypeQuestionCalculateSuccessResponse extends BaseSwagger @Getter @Schema(description = "MBTI + 애착유형 상세 결과 조회 성공 응답") - public static class LoveTypeMbtiResultSuccessResponse extends BaseSwaggerResponse { + public static class LoveTypePersonalityTypeResultSuccessResponse extends BaseSwaggerResponse { } @Getter @@ -289,10 +289,10 @@ public static class MemberData { private RelationshipStatus relationshipStatus; @Schema(description = "내 MBTI", example = "INTJ") - private String mbti; + private String personalityType; @Schema(description = "상대방 MBTI", example = "ENFP") - private String partnerMbti; + private String otherPersonalityType; @Schema(description = "상대방 애착 유형", example = "UNKNOWN") private PartnerLoveTypeCategory partnerLoveTypeCategory; @@ -303,7 +303,7 @@ public static class MemberData { @Schema(description = "[Deprecated] 상대 프로필 조회 응답 데이터") public static class PartnerMemberData { @Schema(description = "상대방 MBTI", example = "ENFP") - private String mbti; + private String personalityType; @Schema(description = "상대방 애착 유형", example = "UNKNOWN") private PartnerLoveTypeCategory loveTypeCategory; @@ -316,7 +316,7 @@ public static class PartnerMemberData { @Schema(description = "상대 프로필 응답 데이터") public static class PartnerProfileData { @Schema(description = "상대방 MBTI", example = "ENFP") - private String mbti; + private String personalityType; @Schema(description = "상대방 애착 유형", example = "UNKNOWN") private PartnerLoveTypeCategory loveTypeCategory; @@ -335,7 +335,7 @@ public static class UpdateMemberData { private RelationshipStatus relationshipStatus; @Schema(description = "내 MBTI", example = "INTJ") - private String mbti; + private String personalityType; @Schema(description = "내 애착 유형", example = "STABLE_TYPE") private LoveTypeCategory loveTypeCategory; @@ -418,9 +418,9 @@ public static class LoveTypeQuestionCalculationData { @Getter @Schema(description = "MBTI + 애착유형 상세 결과 응답 데이터") - public static class LoveTypeMbtiResultData { - @Schema(description = "MBTI", example = "ENFP") - private String mbti; + public static class LoveTypePersonalityTypeResultData { + @Schema(description = "personalityType", example = "ENFP") + private String personalityType; @Schema(description = "애착 유형", example = "STABLE_TYPE") private LoveTypeCategory loveTypeCategory; @@ -447,10 +447,10 @@ public static class LoveTypeMbtiResultData { private List datingGuides; @Schema(description = "잘 맞는 MBTI 목록") - private List bestMatches; + private List bestMatches; @Schema(description = "부딪히기 쉬운 MBTI 목록") - private List worstMatches; + private List worstMatches; } @Getter @@ -465,9 +465,9 @@ public static class LoveTypeTextBlockData { @Getter @Schema(description = "MBTI + 설명 블록") - public static class LoveTypeMbtiBlockData { - @Schema(description = "MBTI", example = "INFJ") - private String mbti; + public static class LoveTypePersonalityTypeBlockData { + @Schema(description = "personalityType", example = "INFJ") + private String personalityType; @Schema(description = "설명", example = "속마음을 깊이 이해해주며 안정적인 감정을 공유하는 궁합") private String description; diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiFeaturePersistenceAdapter.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiFeaturePersistenceAdapter.java deleted file mode 100644 index 1bc598e6..00000000 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiFeaturePersistenceAdapter.java +++ /dev/null @@ -1,25 +0,0 @@ -package makeus.cmc.malmo.adaptor.out.persistence.adapter; - -import lombok.RequiredArgsConstructor; -import makeus.cmc.malmo.adaptor.out.persistence.mapper.LoveTypeMbtiFeatureMapper; -import makeus.cmc.malmo.adaptor.out.persistence.repository.LoveTypeMbtiFeatureRepository; -import makeus.cmc.malmo.application.port.out.LoadLoveTypeMbtiFeaturePort; -import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiFeature; -import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@Component -@RequiredArgsConstructor -public class LoveTypeMbtiFeaturePersistenceAdapter implements LoadLoveTypeMbtiFeaturePort { - - private final LoveTypeMbtiFeatureRepository loveTypeMbtiFeatureRepository; - private final LoveTypeMbtiFeatureMapper loveTypeMbtiFeatureMapper; - - @Override - public Optional loadByMbtiAndLoveTypeCategory(String mbti, LoveTypeCategory loveTypeCategory) { - return loveTypeMbtiFeatureRepository.findByMbtiIgnoreCaseAndLoveTypeCategory(mbti, loveTypeCategory) - .map(loveTypeMbtiFeatureMapper::toDomain); - } -} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiPromptPersistenceAdapter.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiPromptPersistenceAdapter.java deleted file mode 100644 index 700e715c..00000000 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypeMbtiPromptPersistenceAdapter.java +++ /dev/null @@ -1,25 +0,0 @@ -package makeus.cmc.malmo.adaptor.out.persistence.adapter; - -import lombok.RequiredArgsConstructor; -import makeus.cmc.malmo.adaptor.out.persistence.mapper.LoveTypeMbtiPromptMapper; -import makeus.cmc.malmo.adaptor.out.persistence.repository.LoveTypeMbtiPromptRepository; -import makeus.cmc.malmo.application.port.out.LoadLoveTypeMbtiPromptPort; -import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiPrompt; -import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@Component -@RequiredArgsConstructor -public class LoveTypeMbtiPromptPersistenceAdapter implements LoadLoveTypeMbtiPromptPort { - - private final LoveTypeMbtiPromptRepository loveTypeMbtiPromptRepository; - private final LoveTypeMbtiPromptMapper loveTypeMbtiPromptMapper; - - @Override - public Optional loadByMbtiAndLoveTypeCategory(String mbti, LoveTypeCategory loveTypeCategory) { - return loveTypeMbtiPromptRepository.findByMbtiIgnoreCaseAndLoveTypeCategory(mbti, loveTypeCategory) - .map(loveTypeMbtiPromptMapper::toDomain); - } -} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypePersonalityTypeFeaturePersistenceAdapter.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypePersonalityTypeFeaturePersistenceAdapter.java new file mode 100644 index 00000000..7ff43d3a --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypePersonalityTypeFeaturePersistenceAdapter.java @@ -0,0 +1,29 @@ +package makeus.cmc.malmo.adaptor.out.persistence.adapter; + +import lombok.RequiredArgsConstructor; +import makeus.cmc.malmo.adaptor.out.persistence.mapper.LoveTypePersonalityTypeFeatureMapper; +import makeus.cmc.malmo.adaptor.out.persistence.repository.LoveTypePersonalityTypeFeatureRepository; +import makeus.cmc.malmo.application.port.out.LoadLoveTypePersonalityTypeFeaturePort; +import makeus.cmc.malmo.domain.model.love_type.LoveTypePersonalityTypeFeature; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class LoveTypePersonalityTypeFeaturePersistenceAdapter implements LoadLoveTypePersonalityTypeFeaturePort { + + private final LoveTypePersonalityTypeFeatureRepository loveTypePersonalityTypeFeatureRepository; + private final LoveTypePersonalityTypeFeatureMapper loveTypePersonalityTypeFeatureMapper; + + @Override + public Optional loadByPersonalityTypeAndLoveTypeCategory( + String personalityType, + LoveTypeCategory loveTypeCategory + ) { + return loveTypePersonalityTypeFeatureRepository + .findByPersonalityTypeIgnoreCaseAndLoveTypeCategory(personalityType, loveTypeCategory) + .map(loveTypePersonalityTypeFeatureMapper::toDomain); + } +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypePersonalityTypePromptPersistenceAdapter.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypePersonalityTypePromptPersistenceAdapter.java new file mode 100644 index 00000000..e74b1a5b --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/LoveTypePersonalityTypePromptPersistenceAdapter.java @@ -0,0 +1,29 @@ +package makeus.cmc.malmo.adaptor.out.persistence.adapter; + +import lombok.RequiredArgsConstructor; +import makeus.cmc.malmo.adaptor.out.persistence.mapper.LoveTypePersonalityTypePromptMapper; +import makeus.cmc.malmo.adaptor.out.persistence.repository.LoveTypePersonalityTypePromptRepository; +import makeus.cmc.malmo.application.port.out.LoadLoveTypePersonalityTypePromptPort; +import makeus.cmc.malmo.domain.model.love_type.LoveTypePersonalityTypePrompt; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class LoveTypePersonalityTypePromptPersistenceAdapter implements LoadLoveTypePersonalityTypePromptPort { + + private final LoveTypePersonalityTypePromptRepository loveTypePersonalityTypePromptRepository; + private final LoveTypePersonalityTypePromptMapper loveTypePersonalityTypePromptMapper; + + @Override + public Optional loadByPersonalityTypeAndLoveTypeCategory( + String personalityType, + LoveTypeCategory loveTypeCategory + ) { + return loveTypePersonalityTypePromptRepository + .findByPersonalityTypeIgnoreCaseAndLoveTypeCategory(personalityType, loveTypeCategory) + .map(loveTypePersonalityTypePromptMapper::toDomain); + } +} 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 757e5d71..6e415871 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 @@ -105,8 +105,8 @@ public static class MemberResponseRepositoryDto { private String nickname; private String email; private RelationshipStatus relationshipStatus; - private String mbti; - private String partnerMbti; + private String personalityType; + private String otherPersonalityType; private PartnerLoveTypeCategory partnerLoveTypeCategory; public MemberQueryHelper.MemberInfoDto toDto(int totalChatRoomCount, int totalCoupleQuestionCount) { @@ -122,8 +122,8 @@ public MemberQueryHelper.MemberInfoDto toDto(int totalChatRoomCount, int totalCo .totalChatRoomCount(totalChatRoomCount) .totalCoupleQuestionCount(totalCoupleQuestionCount) .relationshipStatus(relationshipStatus) - .mbti(mbti) - .partnerMbti(partnerMbti) + .personalityType(personalityType) + .otherPersonalityType(otherPersonalityType) .partnerLoveTypeCategory(partnerLoveTypeCategory) .build(); } @@ -132,12 +132,12 @@ public MemberQueryHelper.MemberInfoDto toDto(int totalChatRoomCount, int totalCo @Data @AllArgsConstructor public static class PartnerMemberRepositoryDto { - private String mbti; + private String personalityType; private PartnerLoveTypeCategory loveTypeCategory; public MemberQueryHelper.PartnerMemberDto toDto() { return MemberQueryHelper.PartnerMemberDto.builder() - .mbti(mbti) + .personalityType(personalityType) .loveTypeCategory(loveTypeCategory) .description(loveTypeCategory == null ? null : loveTypeCategory.getDescription()) .build(); diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypeMbtiFeatureEntity.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypePersonalityTypeFeatureEntity.java similarity index 84% rename from src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypeMbtiFeatureEntity.java rename to src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypePersonalityTypeFeatureEntity.java index 1bda7855..4685d2ae 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypeMbtiFeatureEntity.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypePersonalityTypeFeatureEntity.java @@ -12,7 +12,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import makeus.cmc.malmo.adaptor.out.persistence.entity.value.LoveTypeMbtiFeatureEntityId; +import makeus.cmc.malmo.adaptor.out.persistence.entity.value.LoveTypePersonalityTypeFeatureEntityId; import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; @Getter @@ -20,13 +20,13 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -@IdClass(LoveTypeMbtiFeatureEntityId.class) -@Table(name = "love_type_mbti_feature") -public class LoveTypeMbtiFeatureEntity { +@IdClass(LoveTypePersonalityTypeFeatureEntityId.class) +@Table(name = "love_type_personality_type_feature") +public class LoveTypePersonalityTypeFeatureEntity { @Id - @Column(name = "mbti") - private String mbti; + @Column(name = "personality_type") + private String personalityType; @Id @Column(name = "lovetype") @@ -126,26 +126,26 @@ public class LoveTypeMbtiFeatureEntity { @Column(name = "dating_guide3") private String datingGuide3; - @Column(name = "best_mbti1") - private String bestMbti1; + @Column(name = "best_personality_type1") + private String bestPersonalityType1; @Column(name = "best_desc1") private String bestDesc1; - @Column(name = "best_mbti2") - private String bestMbti2; + @Column(name = "best_personality_type2") + private String bestPersonalityType2; @Column(name = "best_desc2") private String bestDesc2; - @Column(name = "worst_mbti1") - private String worstMbti1; + @Column(name = "worst_personality_type1") + private String worstPersonalityType1; @Column(name = "worst_desc1") private String worstDesc1; - @Column(name = "worst_mbti2") - private String worstMbti2; + @Column(name = "worst_personality_type2") + private String worstPersonalityType2; @Column(name = "worst_desc2") private String worstDesc2; diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypeMbtiPromptEntity.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypePersonalityTypePromptEntity.java similarity index 77% rename from src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypeMbtiPromptEntity.java rename to src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypePersonalityTypePromptEntity.java index 941c896b..fb6ce95b 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypeMbtiPromptEntity.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/LoveTypePersonalityTypePromptEntity.java @@ -12,7 +12,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import makeus.cmc.malmo.adaptor.out.persistence.entity.value.LoveTypeMbtiPromptEntityId; +import makeus.cmc.malmo.adaptor.out.persistence.entity.value.LoveTypePersonalityTypePromptEntityId; import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; @Getter @@ -20,13 +20,13 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -@IdClass(LoveTypeMbtiPromptEntityId.class) -@Table(name = "love_type_mbti_prompt") -public class LoveTypeMbtiPromptEntity { +@IdClass(LoveTypePersonalityTypePromptEntityId.class) +@Table(name = "love_type_personality_type_prompt") +public class LoveTypePersonalityTypePromptEntity { @Id - @Column(name = "mbti") - private String mbti; + @Column(name = "personality_type") + private String personalityType; @Id @Column(name = "lovetype") diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/member/MemberEntity.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/member/MemberEntity.java index a61a8fa9..c0ab1297 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/member/MemberEntity.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/member/MemberEntity.java @@ -75,9 +75,11 @@ public class MemberEntity extends BaseTimeEntity { @Enumerated(value = EnumType.STRING) private RelationshipStatus relationshipStatus; - private String mbti; + @Column(name = "personality_type") + private String personalityType; - private String partnerMbti; + @Column(name = "other_personality_type") + private String otherPersonalityType; @Enumerated(value = EnumType.STRING) private PartnerLoveTypeCategory partnerLoveTypeCategory; diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypeMbtiPromptEntityId.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypePersonalityTypeFeatureEntityId.java similarity index 76% rename from src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypeMbtiPromptEntityId.java rename to src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypePersonalityTypeFeatureEntityId.java index df098da3..d0f4b543 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypeMbtiPromptEntityId.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypePersonalityTypeFeatureEntityId.java @@ -10,7 +10,7 @@ @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode -public class LoveTypeMbtiPromptEntityId implements Serializable { - private String mbti; +public class LoveTypePersonalityTypeFeatureEntityId implements Serializable { + private String personalityType; private LoveTypeCategory loveTypeCategory; } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypeMbtiFeatureEntityId.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypePersonalityTypePromptEntityId.java similarity index 76% rename from src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypeMbtiFeatureEntityId.java rename to src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypePersonalityTypePromptEntityId.java index 736e1e86..e7d60cbc 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypeMbtiFeatureEntityId.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/value/LoveTypePersonalityTypePromptEntityId.java @@ -10,7 +10,7 @@ @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode -public class LoveTypeMbtiFeatureEntityId implements Serializable { - private String mbti; +public class LoveTypePersonalityTypePromptEntityId implements Serializable { + private String personalityType; private LoveTypeCategory loveTypeCategory; } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypeMbtiPromptMapper.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypeMbtiPromptMapper.java deleted file mode 100644 index 42120aec..00000000 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypeMbtiPromptMapper.java +++ /dev/null @@ -1,21 +0,0 @@ -package makeus.cmc.malmo.adaptor.out.persistence.mapper; - -import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypeMbtiPromptEntity; -import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiPrompt; -import org.springframework.stereotype.Component; - -@Component -public class LoveTypeMbtiPromptMapper { - - public LoveTypeMbtiPrompt toDomain(LoveTypeMbtiPromptEntity entity) { - if (entity == null) { - return null; - } - - return LoveTypeMbtiPrompt.from( - entity.getMbti(), - entity.getLoveTypeCategory(), - entity.getPrompts() - ); - } -} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypeMbtiFeatureMapper.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypePersonalityTypeFeatureMapper.java similarity index 73% rename from src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypeMbtiFeatureMapper.java rename to src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypePersonalityTypeFeatureMapper.java index 97c1644f..5c2b9a22 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypeMbtiFeatureMapper.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypePersonalityTypeFeatureMapper.java @@ -1,19 +1,19 @@ package makeus.cmc.malmo.adaptor.out.persistence.mapper; -import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypeMbtiFeatureEntity; -import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiFeature; +import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypePersonalityTypeFeatureEntity; +import makeus.cmc.malmo.domain.model.love_type.LoveTypePersonalityTypeFeature; import org.springframework.stereotype.Component; @Component -public class LoveTypeMbtiFeatureMapper { +public class LoveTypePersonalityTypeFeatureMapper { - public LoveTypeMbtiFeature toDomain(LoveTypeMbtiFeatureEntity entity) { + public LoveTypePersonalityTypeFeature toDomain(LoveTypePersonalityTypeFeatureEntity entity) { if (entity == null) { return null; } - return LoveTypeMbtiFeature.from( - entity.getMbti(), + return LoveTypePersonalityTypeFeature.from( + entity.getPersonalityType(), entity.getLoveTypeCategory(), entity.getSummary(), entity.getKeyword1(), @@ -46,13 +46,13 @@ public LoveTypeMbtiFeature toDomain(LoveTypeMbtiFeatureEntity entity) { entity.getDatingGuide1(), entity.getDatingGuide2(), entity.getDatingGuide3(), - entity.getBestMbti1(), + entity.getBestPersonalityType1(), entity.getBestDesc1(), - entity.getBestMbti2(), + entity.getBestPersonalityType2(), entity.getBestDesc2(), - entity.getWorstMbti1(), + entity.getWorstPersonalityType1(), entity.getWorstDesc1(), - entity.getWorstMbti2(), + entity.getWorstPersonalityType2(), entity.getWorstDesc2() ); } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypePersonalityTypePromptMapper.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypePersonalityTypePromptMapper.java new file mode 100644 index 00000000..cca3d908 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/LoveTypePersonalityTypePromptMapper.java @@ -0,0 +1,21 @@ +package makeus.cmc.malmo.adaptor.out.persistence.mapper; + +import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypePersonalityTypePromptEntity; +import makeus.cmc.malmo.domain.model.love_type.LoveTypePersonalityTypePrompt; +import org.springframework.stereotype.Component; + +@Component +public class LoveTypePersonalityTypePromptMapper { + + public LoveTypePersonalityTypePrompt toDomain(LoveTypePersonalityTypePromptEntity entity) { + if (entity == null) { + return null; + } + + return LoveTypePersonalityTypePrompt.from( + entity.getPersonalityType(), + entity.getLoveTypeCategory(), + entity.getPrompts() + ); + } +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/MemberMapper.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/MemberMapper.java index f0d09fff..4225dadb 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/MemberMapper.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/MemberMapper.java @@ -34,8 +34,8 @@ public Member toDomain(MemberEntity entity) { entity.getOauthToken(), entity.getCoupleEntityId() != null ? CoupleId.of(entity.getCoupleEntityId().getValue()) : null, entity.getRelationshipStatus(), - entity.getMbti(), - entity.getPartnerMbti(), + entity.getPersonalityType(), + entity.getOtherPersonalityType(), entity.getPartnerLoveTypeCategory(), entity.getCreatedAt(), entity.getModifiedAt(), @@ -66,8 +66,8 @@ public MemberEntity toEntity(Member domain) { .oauthToken(domain.getOauthToken()) .coupleEntityId(domain.getCoupleId() != null ? CoupleEntityId.of(domain.getCoupleId().getValue()) : null) .relationshipStatus(domain.getRelationshipStatus()) - .mbti(domain.getMbti()) - .partnerMbti(domain.getPartnerMbti()) + .personalityType(domain.getPersonalityType()) + .otherPersonalityType(domain.getOtherPersonalityType()) .partnerLoveTypeCategory(domain.getPartnerLoveTypeCategory()) .createdAt(domain.getCreatedAt()) .modifiedAt(domain.getModifiedAt()) diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiFeatureRepository.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiFeatureRepository.java deleted file mode 100644 index fae51a58..00000000 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiFeatureRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package makeus.cmc.malmo.adaptor.out.persistence.repository; - -import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypeMbtiFeatureEntity; -import makeus.cmc.malmo.adaptor.out.persistence.entity.value.LoveTypeMbtiFeatureEntityId; -import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface LoveTypeMbtiFeatureRepository extends JpaRepository { - Optional findByMbtiIgnoreCaseAndLoveTypeCategory(String mbti, LoveTypeCategory loveTypeCategory); -} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiPromptRepository.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiPromptRepository.java deleted file mode 100644 index b778c472..00000000 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypeMbtiPromptRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package makeus.cmc.malmo.adaptor.out.persistence.repository; - -import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypeMbtiPromptEntity; -import makeus.cmc.malmo.adaptor.out.persistence.entity.value.LoveTypeMbtiPromptEntityId; -import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface LoveTypeMbtiPromptRepository extends JpaRepository { - Optional findByMbtiIgnoreCaseAndLoveTypeCategory(String mbti, LoveTypeCategory loveTypeCategory); -} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypePersonalityTypeFeatureRepository.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypePersonalityTypeFeatureRepository.java new file mode 100644 index 00000000..6200464e --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypePersonalityTypeFeatureRepository.java @@ -0,0 +1,16 @@ +package makeus.cmc.malmo.adaptor.out.persistence.repository; + +import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypePersonalityTypeFeatureEntity; +import makeus.cmc.malmo.adaptor.out.persistence.entity.value.LoveTypePersonalityTypeFeatureEntityId; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface LoveTypePersonalityTypeFeatureRepository + extends JpaRepository { + Optional findByPersonalityTypeIgnoreCaseAndLoveTypeCategory( + String personalityType, + LoveTypeCategory loveTypeCategory + ); +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypePersonalityTypePromptRepository.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypePersonalityTypePromptRepository.java new file mode 100644 index 00000000..c4f58ae6 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/LoveTypePersonalityTypePromptRepository.java @@ -0,0 +1,16 @@ +package makeus.cmc.malmo.adaptor.out.persistence.repository; + +import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypePersonalityTypePromptEntity; +import makeus.cmc.malmo.adaptor.out.persistence.entity.value.LoveTypePersonalityTypePromptEntityId; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface LoveTypePersonalityTypePromptRepository + extends JpaRepository { + Optional findByPersonalityTypeIgnoreCaseAndLoveTypeCategory( + String personalityType, + LoveTypeCategory loveTypeCategory + ); +} 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 f3965462..ad74aadc 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 @@ -34,8 +34,8 @@ public Optional findMember memberEntity.nickname, memberEntity.email, memberEntity.relationshipStatus, - memberEntity.mbti, - memberEntity.partnerMbti, + memberEntity.personalityType, + memberEntity.otherPersonalityType, memberEntity.partnerLoveTypeCategory )) .from(memberEntity) @@ -52,12 +52,12 @@ public Optional findMember public Optional findPartnerMember(Long memberId) { MemberPersistenceAdapter.PartnerMemberRepositoryDto dto = queryFactory .select(Projections.constructor(MemberPersistenceAdapter.PartnerMemberRepositoryDto.class, - memberEntity.partnerMbti, + memberEntity.otherPersonalityType, memberEntity.partnerLoveTypeCategory )) .from(memberEntity) .where(memberEntity.id.eq(memberId) - .and(memberEntity.partnerMbti.isNotNull())) + .and(memberEntity.otherPersonalityType.isNotNull())) .fetchOne(); return Optional.ofNullable(dto); diff --git a/src/main/java/makeus/cmc/malmo/application/exception/LoveTypeMbtiFeatureNotFoundException.java b/src/main/java/makeus/cmc/malmo/application/exception/LoveTypeMbtiFeatureNotFoundException.java deleted file mode 100644 index 45136057..00000000 --- a/src/main/java/makeus/cmc/malmo/application/exception/LoveTypeMbtiFeatureNotFoundException.java +++ /dev/null @@ -1,4 +0,0 @@ -package makeus.cmc.malmo.application.exception; - -public class LoveTypeMbtiFeatureNotFoundException extends RuntimeException { -} diff --git a/src/main/java/makeus/cmc/malmo/application/exception/LoveTypePersonalityTypeFeatureNotFoundException.java b/src/main/java/makeus/cmc/malmo/application/exception/LoveTypePersonalityTypeFeatureNotFoundException.java new file mode 100644 index 00000000..a26475ed --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/exception/LoveTypePersonalityTypeFeatureNotFoundException.java @@ -0,0 +1,4 @@ +package makeus.cmc.malmo.application.exception; + +public class LoveTypePersonalityTypeFeatureNotFoundException extends RuntimeException { +} diff --git a/src/main/java/makeus/cmc/malmo/application/helper/love_type/LoveTypeMbtiPromptQueryHelper.java b/src/main/java/makeus/cmc/malmo/application/helper/love_type/LoveTypeMbtiPromptQueryHelper.java deleted file mode 100644 index d57950fd..00000000 --- a/src/main/java/makeus/cmc/malmo/application/helper/love_type/LoveTypeMbtiPromptQueryHelper.java +++ /dev/null @@ -1,20 +0,0 @@ -package makeus.cmc.malmo.application.helper.love_type; - -import lombok.RequiredArgsConstructor; -import makeus.cmc.malmo.application.port.out.LoadLoveTypeMbtiPromptPort; -import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiPrompt; -import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@Component -@RequiredArgsConstructor -public class LoveTypeMbtiPromptQueryHelper { - - private final LoadLoveTypeMbtiPromptPort loadLoveTypeMbtiPromptPort; - - public Optional findByMbtiAndLoveTypeCategory(String mbti, LoveTypeCategory loveTypeCategory) { - return loadLoveTypeMbtiPromptPort.loadByMbtiAndLoveTypeCategory(mbti, loveTypeCategory); - } -} diff --git a/src/main/java/makeus/cmc/malmo/application/helper/love_type/LoveTypePersonalityTypePromptQueryHelper.java b/src/main/java/makeus/cmc/malmo/application/helper/love_type/LoveTypePersonalityTypePromptQueryHelper.java new file mode 100644 index 00000000..ab367fc8 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/helper/love_type/LoveTypePersonalityTypePromptQueryHelper.java @@ -0,0 +1,24 @@ +package makeus.cmc.malmo.application.helper.love_type; + +import lombok.RequiredArgsConstructor; +import makeus.cmc.malmo.application.port.out.LoadLoveTypePersonalityTypePromptPort; +import makeus.cmc.malmo.domain.model.love_type.LoveTypePersonalityTypePrompt; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class LoveTypePersonalityTypePromptQueryHelper { + + private final LoadLoveTypePersonalityTypePromptPort loadLoveTypePersonalityTypePromptPort; + + public Optional findByPersonalityTypeAndLoveTypeCategory( + String personalityType, + LoveTypeCategory loveTypeCategory + ) { + return loadLoveTypePersonalityTypePromptPort + .loadByPersonalityTypeAndLoveTypeCategory(personalityType, loveTypeCategory); + } +} 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 0556d7f7..a87f0168 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 @@ -104,15 +104,15 @@ public static class MemberInfoDto { private int totalCoupleQuestionCount; private RelationshipStatus relationshipStatus; - private String mbti; - private String partnerMbti; + private String personalityType; + private String otherPersonalityType; private PartnerLoveTypeCategory partnerLoveTypeCategory; } @Data @Builder public static class PartnerMemberDto { - private String mbti; + private String personalityType; private PartnerLoveTypeCategory loveTypeCategory; private String description; } diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/GetLoveTypeMbtiResultUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/GetLoveTypePersonalityTypeResultUseCase.java similarity index 60% rename from src/main/java/makeus/cmc/malmo/application/port/in/GetLoveTypeMbtiResultUseCase.java rename to src/main/java/makeus/cmc/malmo/application/port/in/GetLoveTypePersonalityTypeResultUseCase.java index 998a7e3a..1ef4e8bb 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/in/GetLoveTypeMbtiResultUseCase.java +++ b/src/main/java/makeus/cmc/malmo/application/port/in/GetLoveTypePersonalityTypeResultUseCase.java @@ -6,21 +6,21 @@ import java.util.List; -public interface GetLoveTypeMbtiResultUseCase { +public interface GetLoveTypePersonalityTypeResultUseCase { - LoveTypeMbtiResultResponse getResult(GetLoveTypeMbtiResultCommand command); + LoveTypePersonalityTypeResultResponse getResult(GetLoveTypePersonalityTypeResultCommand command); @Data @Builder - class GetLoveTypeMbtiResultCommand { - private String mbti; + class GetLoveTypePersonalityTypeResultCommand { + private String personalityType; private LoveTypeCategory loveTypeCategory; } @Data @Builder - class LoveTypeMbtiResultResponse { - private String mbti; + class LoveTypePersonalityTypeResultResponse { + private String personalityType; private LoveTypeCategory loveTypeCategory; private String summary; private List keywords; @@ -29,8 +29,8 @@ class LoveTypeMbtiResultResponse { private List patterns; private List loveTypeFeatures; private List datingGuides; - private List bestMatches; - private List worstMatches; + private List bestMatches; + private List worstMatches; } @Data @@ -42,8 +42,8 @@ class TitleDescriptionItem { @Data @Builder - class MbtiDescriptionItem { - private String mbti; + class PersonalityTypeDescriptionItem { + private String personalityType; private String description; } } diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/member/CreatePartnerProfileUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/member/CreatePartnerProfileUseCase.java index bab5ea6d..6266c561 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/in/member/CreatePartnerProfileUseCase.java +++ b/src/main/java/makeus/cmc/malmo/application/port/in/member/CreatePartnerProfileUseCase.java @@ -12,14 +12,14 @@ public interface CreatePartnerProfileUseCase { @Builder class CreatePartnerProfileCommand { private Long memberId; - private String mbti; + private String personalityType; private PartnerLoveTypeCategory loveTypeCategory; } @Data @Builder class PartnerProfileResponseDto { - private String mbti; + private String personalityType; private PartnerLoveTypeCategory loveTypeCategory; private String description; } diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/member/GetMemberUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/member/GetMemberUseCase.java index 0bb9aabd..df099ec5 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/in/member/GetMemberUseCase.java +++ b/src/main/java/makeus/cmc/malmo/application/port/in/member/GetMemberUseCase.java @@ -38,8 +38,8 @@ class MemberResponseDto { private String email; private RelationshipStatus relationshipStatus; - private String mbti; - private String partnerMbti; + private String personalityType; + private String otherPersonalityType; private PartnerLoveTypeCategory partnerLoveTypeCategory; } } 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 8f4d0f69..09d83848 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 @@ -17,7 +17,7 @@ class PartnerInfoCommand { @Data @Builder class PartnerMemberResponseDto { - private String mbti; + private String personalityType; private PartnerLoveTypeCategory loveTypeCategory; private String description; } diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdateMemberUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdateMemberUseCase.java index 4ff10734..8444aa43 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdateMemberUseCase.java +++ b/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdateMemberUseCase.java @@ -15,7 +15,7 @@ class UpdateMemberCommand { private Long memberId; private String nickname; private RelationshipStatus relationshipStatus; - private String mbti; + private String personalityType; private LoveTypeCategory loveTypeCategory; } @@ -24,7 +24,7 @@ class UpdateMemberCommand { class UpdateMemberResponseDto { private String nickname; private RelationshipStatus relationshipStatus; - private String mbti; + private String personalityType; private LoveTypeCategory loveTypeCategory; } } diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdatePartnerProfileUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdatePartnerProfileUseCase.java index 204b02d3..5f55f76f 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdatePartnerProfileUseCase.java +++ b/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdatePartnerProfileUseCase.java @@ -12,8 +12,8 @@ public interface UpdatePartnerProfileUseCase { @Builder class UpdatePartnerProfileCommand { private Long memberId; - private String mbti; - private boolean mbtiProvided; + private String personalityType; + private boolean personalityTypeProvided; private PartnerLoveTypeCategory loveTypeCategory; private boolean loveTypeCategoryProvided; } diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiFeaturePort.java b/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiFeaturePort.java deleted file mode 100644 index be5b9a0a..00000000 --- a/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiFeaturePort.java +++ /dev/null @@ -1,10 +0,0 @@ -package makeus.cmc.malmo.application.port.out; - -import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiFeature; -import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; - -import java.util.Optional; - -public interface LoadLoveTypeMbtiFeaturePort { - Optional loadByMbtiAndLoveTypeCategory(String mbti, LoveTypeCategory loveTypeCategory); -} diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiPromptPort.java b/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiPromptPort.java deleted file mode 100644 index 808c5dc2..00000000 --- a/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypeMbtiPromptPort.java +++ /dev/null @@ -1,10 +0,0 @@ -package makeus.cmc.malmo.application.port.out; - -import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiPrompt; -import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; - -import java.util.Optional; - -public interface LoadLoveTypeMbtiPromptPort { - Optional loadByMbtiAndLoveTypeCategory(String mbti, LoveTypeCategory loveTypeCategory); -} diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypePersonalityTypeFeaturePort.java b/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypePersonalityTypeFeaturePort.java new file mode 100644 index 00000000..559b5def --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypePersonalityTypeFeaturePort.java @@ -0,0 +1,13 @@ +package makeus.cmc.malmo.application.port.out; + +import makeus.cmc.malmo.domain.model.love_type.LoveTypePersonalityTypeFeature; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; + +import java.util.Optional; + +public interface LoadLoveTypePersonalityTypeFeaturePort { + Optional loadByPersonalityTypeAndLoveTypeCategory( + String personalityType, + LoveTypeCategory loveTypeCategory + ); +} diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypePersonalityTypePromptPort.java b/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypePersonalityTypePromptPort.java new file mode 100644 index 00000000..5f0036e4 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/port/out/LoadLoveTypePersonalityTypePromptPort.java @@ -0,0 +1,13 @@ +package makeus.cmc.malmo.application.port.out; + +import makeus.cmc.malmo.domain.model.love_type.LoveTypePersonalityTypePrompt; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; + +import java.util.Optional; + +public interface LoadLoveTypePersonalityTypePromptPort { + Optional loadByPersonalityTypeAndLoveTypeCategory( + String personalityType, + LoveTypeCategory loveTypeCategory + ); +} diff --git a/src/main/java/makeus/cmc/malmo/application/service/LoveTypeMbtiFeatureService.java b/src/main/java/makeus/cmc/malmo/application/service/LoveTypePersonalityTypeFeatureService.java similarity index 61% rename from src/main/java/makeus/cmc/malmo/application/service/LoveTypeMbtiFeatureService.java rename to src/main/java/makeus/cmc/malmo/application/service/LoveTypePersonalityTypeFeatureService.java index 7c89c49d..aa76548b 100644 --- a/src/main/java/makeus/cmc/malmo/application/service/LoveTypeMbtiFeatureService.java +++ b/src/main/java/makeus/cmc/malmo/application/service/LoveTypePersonalityTypeFeatureService.java @@ -1,10 +1,10 @@ package makeus.cmc.malmo.application.service; import lombok.RequiredArgsConstructor; -import makeus.cmc.malmo.application.exception.LoveTypeMbtiFeatureNotFoundException; -import makeus.cmc.malmo.application.port.in.GetLoveTypeMbtiResultUseCase; -import makeus.cmc.malmo.application.port.out.LoadLoveTypeMbtiFeaturePort; -import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiFeature; +import makeus.cmc.malmo.application.exception.LoveTypePersonalityTypeFeatureNotFoundException; +import makeus.cmc.malmo.application.port.in.GetLoveTypePersonalityTypeResultUseCase; +import makeus.cmc.malmo.application.port.out.LoadLoveTypePersonalityTypeFeaturePort; +import makeus.cmc.malmo.domain.model.love_type.LoveTypePersonalityTypeFeature; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -13,18 +13,18 @@ @Service @RequiredArgsConstructor -public class LoveTypeMbtiFeatureService implements GetLoveTypeMbtiResultUseCase { +public class LoveTypePersonalityTypeFeatureService implements GetLoveTypePersonalityTypeResultUseCase { - private final LoadLoveTypeMbtiFeaturePort loadLoveTypeMbtiFeaturePort; + private final LoadLoveTypePersonalityTypeFeaturePort loadLoveTypePersonalityTypeFeaturePort; @Override - public LoveTypeMbtiResultResponse getResult(GetLoveTypeMbtiResultCommand command) { - LoveTypeMbtiFeature feature = loadLoveTypeMbtiFeaturePort - .loadByMbtiAndLoveTypeCategory(command.getMbti(), command.getLoveTypeCategory()) - .orElseThrow(LoveTypeMbtiFeatureNotFoundException::new); + public LoveTypePersonalityTypeResultResponse getResult(GetLoveTypePersonalityTypeResultCommand command) { + LoveTypePersonalityTypeFeature feature = loadLoveTypePersonalityTypeFeaturePort + .loadByPersonalityTypeAndLoveTypeCategory(command.getPersonalityType(), command.getLoveTypeCategory()) + .orElseThrow(LoveTypePersonalityTypeFeatureNotFoundException::new); - return LoveTypeMbtiResultResponse.builder() - .mbti(feature.getMbti()) + return LoveTypePersonalityTypeResultResponse.builder() + .personalityType(feature.getPersonalityType()) .loveTypeCategory(feature.getLoveTypeCategory()) .summary(feature.getSummary()) .keywords(buildStringList(feature.getKeyword1(), feature.getKeyword2(), feature.getKeyword3())) @@ -51,13 +51,13 @@ public LoveTypeMbtiResultResponse getResult(GetLoveTypeMbtiResultCommand command feature.getDatingGuide2(), feature.getDatingGuide3() )) - .bestMatches(buildMbtiDescriptionItems( - feature.getBestMbti1(), feature.getBestDesc1(), - feature.getBestMbti2(), feature.getBestDesc2() + .bestMatches(buildPersonalityTypeDescriptionItems( + feature.getBestPersonalityType1(), feature.getBestDesc1(), + feature.getBestPersonalityType2(), feature.getBestDesc2() )) - .worstMatches(buildMbtiDescriptionItems( - feature.getWorstMbti1(), feature.getWorstDesc1(), - feature.getWorstMbti2(), feature.getWorstDesc2() + .worstMatches(buildPersonalityTypeDescriptionItems( + feature.getWorstPersonalityType1(), feature.getWorstDesc1(), + feature.getWorstPersonalityType2(), feature.getWorstDesc2() )) .build(); } @@ -78,13 +78,13 @@ private List buildTitleDescriptionItems(String... values) .toList(); } - private List buildMbtiDescriptionItems(String... values) { + private List buildPersonalityTypeDescriptionItems(String... values) { return Stream.iterate(0, index -> index < values.length, index -> index + 2) - .map(index -> MbtiDescriptionItem.builder() - .mbti(normalizeBlank(values[index])) + .map(index -> PersonalityTypeDescriptionItem.builder() + .personalityType(normalizeBlank(values[index])) .description(normalizeBlank(values[index + 1])) .build()) - .filter(item -> StringUtils.hasText(item.getMbti()) || StringUtils.hasText(item.getDescription())) + .filter(item -> StringUtils.hasText(item.getPersonalityType()) || StringUtils.hasText(item.getDescription())) .toList(); } 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 fcc40d15..fdb46d94 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 @@ -4,7 +4,7 @@ import lombok.extern.slf4j.Slf4j; import makeus.cmc.malmo.application.helper.chat_room.ChatRoomQueryHelper; import makeus.cmc.malmo.application.helper.chat_room.MemberChatRoomMetadataQueryHelper; -import makeus.cmc.malmo.application.helper.love_type.LoveTypeMbtiPromptQueryHelper; +import makeus.cmc.malmo.application.helper.love_type.LoveTypePersonalityTypePromptQueryHelper; import makeus.cmc.malmo.application.port.out.chat.LoadChatRoomMetadataPort; import makeus.cmc.malmo.domain.model.chat.ChatMessage; import makeus.cmc.malmo.domain.model.chat.ChatRoom; @@ -33,7 +33,7 @@ public class ChatPromptBuilder { private final ChatRoomQueryHelper chatRoomQueryHelper; private final MemberChatRoomMetadataQueryHelper memberChatRoomMetadataQueryHelper; - private final LoveTypeMbtiPromptQueryHelper loveTypeMbtiPromptQueryHelper; + private final LoveTypePersonalityTypePromptQueryHelper loveTypePersonalityTypePromptQueryHelper; public List> createForProcessUserMessage(Member member, ChatRoom chatRoom, String userMessage) { List> messages = new ArrayList<>(); @@ -90,11 +90,11 @@ private String getMetaDataContent(Member member) { String relationshipStatus = member.getRelationshipStatus() != null ? member.getRelationshipStatus().name() : "알 수 없음"; metadataBuilder.append("- 사용자 연애 상태: ").append(relationshipStatus).append("\n"); - String mbti = member.getMbti() != null ? member.getMbti() : "알 수 없음"; - metadataBuilder.append("- 사용자 MBTI: ").append(mbti).append("\n"); + String personalityType = member.getPersonalityType() != null ? member.getPersonalityType() : "알 수 없음"; + metadataBuilder.append("- 사용자 MBTI: ").append(personalityType).append("\n"); - String partnerMbti = member.getPartnerMbti() != null ? member.getPartnerMbti() : "알 수 없음"; - metadataBuilder.append("- 상대방 MBTI: ").append(partnerMbti).append("\n"); + String otherPersonalityType = member.getOtherPersonalityType() != null ? member.getOtherPersonalityType() : "알 수 없음"; + metadataBuilder.append("- 상대방 MBTI: ").append(otherPersonalityType).append("\n"); // String dDayState = memberDomainService.getMemberDDayState(member.getStartLoveDate()); // metadataBuilder.append("- 연애 기간: ").append(dDayState).append("\n"); @@ -109,7 +109,7 @@ private String getMetaDataContent(Member member) { .append(resolveMemberPrompt(member)) .append("\n"); - if (StringUtils.hasText(member.getPartnerMbti())) { + if (StringUtils.hasText(member.getOtherPersonalityType())) { metadataBuilder.append("- 상대방 성향 프롬프트:\n") .append(resolvePartnerPrompt(member)) .append("\n"); @@ -121,22 +121,23 @@ private String getMetaDataContent(Member member) { } private String resolveMemberPrompt(Member member) { - if (!StringUtils.hasText(member.getMbti()) || member.getLoveTypeCategory() == null) { + if (!StringUtils.hasText(member.getPersonalityType()) || member.getLoveTypeCategory() == null) { return UNKNOWN_INFERENCE_PROMPT; } - return loveTypeMbtiPromptQueryHelper.findByMbtiAndLoveTypeCategory(member.getMbti(), member.getLoveTypeCategory()) + return loveTypePersonalityTypePromptQueryHelper + .findByPersonalityTypeAndLoveTypeCategory(member.getPersonalityType(), member.getLoveTypeCategory()) .map(prompt -> prompt.getPrompts()) .filter(StringUtils::hasText) .orElseGet(() -> { - log.warn("Missing love_type_mbti_prompt row for member. mbti={}, loveType={}", - member.getMbti(), member.getLoveTypeCategory()); + log.warn("Missing love_type_personality_type_prompt row for member. personalityType={}, loveType={}", + member.getPersonalityType(), member.getLoveTypeCategory()); return UNKNOWN_INFERENCE_PROMPT; }); } private String resolvePartnerPrompt(Member member) { - if (!StringUtils.hasText(member.getPartnerMbti())) { + if (!StringUtils.hasText(member.getOtherPersonalityType())) { return UNKNOWN_INFERENCE_PROMPT; } @@ -145,12 +146,13 @@ private String resolvePartnerPrompt(Member member) { } LoveTypeCategory partnerLoveTypeCategory = LoveTypeCategory.valueOf(member.getPartnerLoveTypeCategory().name()); - return loveTypeMbtiPromptQueryHelper.findByMbtiAndLoveTypeCategory(member.getPartnerMbti(), partnerLoveTypeCategory) + return loveTypePersonalityTypePromptQueryHelper + .findByPersonalityTypeAndLoveTypeCategory(member.getOtherPersonalityType(), partnerLoveTypeCategory) .map(prompt -> prompt.getPrompts()) .filter(StringUtils::hasText) .orElseGet(() -> { - log.warn("Missing love_type_mbti_prompt row for partner. memberId={}, partnerMbti={}, partnerLoveType={}", - member.getId(), member.getPartnerMbti(), partnerLoveTypeCategory); + log.warn("Missing love_type_personality_type_prompt row for partner. memberId={}, otherPersonalityType={}, partnerLoveType={}", + member.getId(), member.getOtherPersonalityType(), partnerLoveTypeCategory); return UNKNOWN_INFERENCE_PROMPT; }); } 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 bc5cf147..fcca26e3 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 @@ -61,7 +61,7 @@ public UpdateMemberResponseDto updateMember(UpdateMemberCommand command) { member.updateMemberProfile( command.getNickname(), command.getRelationshipStatus(), - command.getMbti(), + command.getPersonalityType(), command.getLoveTypeCategory() ); @@ -70,7 +70,7 @@ public UpdateMemberResponseDto updateMember(UpdateMemberCommand command) { return UpdateMemberResponseDto.builder() .nickname(savedMember.getNickname()) .relationshipStatus(savedMember.getRelationshipStatus()) - .mbti(savedMember.getMbti()) + .personalityType(savedMember.getPersonalityType()) .loveTypeCategory(savedMember.getLoveTypeCategory()) .build(); } @@ -85,7 +85,7 @@ public PartnerProfileResponseDto createPartnerProfile(CreatePartnerProfileComman } PartnerLoveTypeCategory partnerLoveTypeCategory = resolvePartnerLoveTypeCategory(command.getLoveTypeCategory()); - member.createPartnerProfile(command.getMbti(), partnerLoveTypeCategory); + member.createPartnerProfile(command.getPersonalityType(), partnerLoveTypeCategory); Member savedMember = memberCommandHelper.saveMember(member); return toPartnerProfileResponse(savedMember); @@ -100,13 +100,13 @@ public PartnerProfileResponseDto updatePartnerProfile(UpdatePartnerProfileComman throw new PartnerProfileNotFoundException("등록된 상대 프로필이 없습니다."); } - String partnerMbti = command.isMbtiProvided() ? command.getMbti() : null; + String otherPersonalityType = command.isPersonalityTypeProvided() ? command.getPersonalityType() : null; PartnerLoveTypeCategory partnerLoveTypeCategory = null; if (command.isLoveTypeCategoryProvided()) { partnerLoveTypeCategory = resolvePartnerLoveTypeCategory(command.getLoveTypeCategory()); } - member.updatePartnerProfile(partnerMbti, partnerLoveTypeCategory); + member.updatePartnerProfile(otherPersonalityType, partnerLoveTypeCategory); Member savedMember = memberCommandHelper.saveMember(member); return toPartnerProfileResponse(savedMember); @@ -181,7 +181,7 @@ public void coupleUnlink(Member member, Couple couple) { private PartnerProfileResponseDto toPartnerProfileResponse(Member savedMember) { return PartnerProfileResponseDto.builder() - .mbti(savedMember.getPartnerMbti()) + .personalityType(savedMember.getOtherPersonalityType()) .loveTypeCategory(savedMember.getPartnerLoveTypeCategory()) .description(savedMember.getPartnerLoveTypeCategory() == null ? null 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 f187ffa1..8df6a00c 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 @@ -32,8 +32,8 @@ public MemberResponseDto getMemberInfo(MemberInfoCommand command) { .nickname(member.getNickname()) .email(member.getEmail()) .relationshipStatus(member.getRelationshipStatus()) - .mbti(member.getMbti()) - .partnerMbti(member.getPartnerMbti()) + .personalityType(member.getPersonalityType()) + .otherPersonalityType(member.getOtherPersonalityType()) .partnerLoveTypeCategory(member.getPartnerLoveTypeCategory()) .build(); } @@ -44,7 +44,7 @@ public PartnerMemberResponseDto getPartnerInfo(PartnerInfoCommand command) { MemberQueryHelper.PartnerMemberDto partner = memberQueryHelper.getPartnerInfoOrThrow(MemberId.of(command.getUserId())); return PartnerMemberResponseDto.builder() - .mbti(partner.getMbti()) + .personalityType(partner.getPersonalityType()) .loveTypeCategory(partner.getLoveTypeCategory()) .description(partner.getDescription()) .build(); diff --git a/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypeMbtiPrompt.java b/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypeMbtiPrompt.java deleted file mode 100644 index b933c16c..00000000 --- a/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypeMbtiPrompt.java +++ /dev/null @@ -1,26 +0,0 @@ -package makeus.cmc.malmo.domain.model.love_type; - -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; - -@Getter -@Builder(access = AccessLevel.PRIVATE) -public class LoveTypeMbtiPrompt { - private String mbti; - private LoveTypeCategory loveTypeCategory; - private String prompts; - - public static LoveTypeMbtiPrompt from(String mbti, LoveTypeCategory loveTypeCategory, String prompts) { - return LoveTypeMbtiPrompt.builder() - .mbti(normalizeMbti(mbti)) - .loveTypeCategory(loveTypeCategory) - .prompts(prompts) - .build(); - } - - private static String normalizeMbti(String mbti) { - return mbti == null ? null : mbti.toUpperCase(); - } -} diff --git a/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypeMbtiFeature.java b/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypePersonalityTypeFeature.java similarity index 78% rename from src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypeMbtiFeature.java rename to src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypePersonalityTypeFeature.java index 954baba7..d7c11ab6 100644 --- a/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypeMbtiFeature.java +++ b/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypePersonalityTypeFeature.java @@ -7,8 +7,8 @@ @Getter @Builder(access = AccessLevel.PRIVATE) -public class LoveTypeMbtiFeature { - private String mbti; +public class LoveTypePersonalityTypeFeature { + private String personalityType; private LoveTypeCategory loveTypeCategory; private String summary; private String keyword1; @@ -41,17 +41,17 @@ public class LoveTypeMbtiFeature { private String datingGuide1; private String datingGuide2; private String datingGuide3; - private String bestMbti1; + private String bestPersonalityType1; private String bestDesc1; - private String bestMbti2; + private String bestPersonalityType2; private String bestDesc2; - private String worstMbti1; + private String worstPersonalityType1; private String worstDesc1; - private String worstMbti2; + private String worstPersonalityType2; private String worstDesc2; - public static LoveTypeMbtiFeature from( - String mbti, + public static LoveTypePersonalityTypeFeature from( + String personalityType, LoveTypeCategory loveTypeCategory, String summary, String keyword1, @@ -84,17 +84,17 @@ public static LoveTypeMbtiFeature from( String datingGuide1, String datingGuide2, String datingGuide3, - String bestMbti1, + String bestPersonalityType1, String bestDesc1, - String bestMbti2, + String bestPersonalityType2, String bestDesc2, - String worstMbti1, + String worstPersonalityType1, String worstDesc1, - String worstMbti2, + String worstPersonalityType2, String worstDesc2 ) { - return LoveTypeMbtiFeature.builder() - .mbti(normalizeMbti(mbti)) + return LoveTypePersonalityTypeFeature.builder() + .personalityType(normalizePersonalityType(personalityType)) .loveTypeCategory(loveTypeCategory) .summary(summary) .keyword1(keyword1) @@ -127,18 +127,18 @@ public static LoveTypeMbtiFeature from( .datingGuide1(datingGuide1) .datingGuide2(datingGuide2) .datingGuide3(datingGuide3) - .bestMbti1(normalizeMbti(bestMbti1)) + .bestPersonalityType1(normalizePersonalityType(bestPersonalityType1)) .bestDesc1(bestDesc1) - .bestMbti2(normalizeMbti(bestMbti2)) + .bestPersonalityType2(normalizePersonalityType(bestPersonalityType2)) .bestDesc2(bestDesc2) - .worstMbti1(normalizeMbti(worstMbti1)) + .worstPersonalityType1(normalizePersonalityType(worstPersonalityType1)) .worstDesc1(worstDesc1) - .worstMbti2(normalizeMbti(worstMbti2)) + .worstPersonalityType2(normalizePersonalityType(worstPersonalityType2)) .worstDesc2(worstDesc2) .build(); } - private static String normalizeMbti(String mbti) { - return mbti == null ? null : mbti.toUpperCase(); + private static String normalizePersonalityType(String personalityType) { + return personalityType == null ? null : personalityType.toUpperCase(); } } diff --git a/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypePersonalityTypePrompt.java b/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypePersonalityTypePrompt.java new file mode 100644 index 00000000..9cdd6658 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/domain/model/love_type/LoveTypePersonalityTypePrompt.java @@ -0,0 +1,30 @@ +package makeus.cmc.malmo.domain.model.love_type; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; + +@Getter +@Builder(access = AccessLevel.PRIVATE) +public class LoveTypePersonalityTypePrompt { + private String personalityType; + private LoveTypeCategory loveTypeCategory; + private String prompts; + + public static LoveTypePersonalityTypePrompt from( + String personalityType, + LoveTypeCategory loveTypeCategory, + String prompts + ) { + return LoveTypePersonalityTypePrompt.builder() + .personalityType(normalizePersonalityType(personalityType)) + .loveTypeCategory(loveTypeCategory) + .prompts(prompts) + .build(); + } + + private static String normalizePersonalityType(String personalityType) { + return personalityType == null ? null : personalityType.toUpperCase(); + } +} 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 6e26d97a..50bcb7f8 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 @@ -46,8 +46,8 @@ public class Member { private CoupleId coupleId; private RelationshipStatus relationshipStatus; - private String mbti; - private String partnerMbti; + private String personalityType; + private String otherPersonalityType; private PartnerLoveTypeCategory partnerLoveTypeCategory; // BaseTimeEntity fields @@ -89,8 +89,8 @@ public static Member from( String oauthToken, CoupleId coupleId, RelationshipStatus relationshipStatus, - String mbti, - String partnerMbti, + String personalityType, + String otherPersonalityType, PartnerLoveTypeCategory partnerLoveTypeCategory, LocalDateTime createdAt, LocalDateTime modifiedAt, @@ -116,8 +116,8 @@ public static Member from( .oauthToken(oauthToken) .coupleId(coupleId) .relationshipStatus(relationshipStatus) - .mbti(normalizeMbti(mbti)) - .partnerMbti(normalizeMbti(partnerMbti)) + .personalityType(normalizePersonalityType(personalityType)) + .otherPersonalityType(normalizePersonalityType(otherPersonalityType)) .partnerLoveTypeCategory(partnerLoveTypeCategory) .createdAt(createdAt) .modifiedAt(modifiedAt) @@ -143,15 +143,15 @@ public void signUp(String nickname, RelationshipStatus relationshipStatus) { this.memberState = MemberState.ALIVE; } - public void updateMemberProfile(String nickname, RelationshipStatus relationshipStatus, String mbti, LoveTypeCategory loveTypeCategory) { + public void updateMemberProfile(String nickname, RelationshipStatus relationshipStatus, String personalityType, LoveTypeCategory loveTypeCategory) { if (nickname != null) { this.nickname = nickname; } if (relationshipStatus != null) { this.relationshipStatus = relationshipStatus; } - if (mbti != null) { - this.mbti = normalizeMbti(mbti); + if (personalityType != null) { + this.personalityType = normalizePersonalityType(personalityType); } if (loveTypeCategory != null) { this.loveTypeCategory = loveTypeCategory; @@ -159,17 +159,17 @@ public void updateMemberProfile(String nickname, RelationshipStatus relationship } public boolean hasPartnerProfile() { - return this.partnerMbti != null; + return this.otherPersonalityType != null; } - public void createPartnerProfile(String partnerMbti, PartnerLoveTypeCategory partnerLoveTypeCategory) { - this.partnerMbti = normalizeMbti(partnerMbti); + public void createPartnerProfile(String otherPersonalityType, PartnerLoveTypeCategory partnerLoveTypeCategory) { + this.otherPersonalityType = normalizePersonalityType(otherPersonalityType); this.partnerLoveTypeCategory = partnerLoveTypeCategory; } - public void updatePartnerProfile(String partnerMbti, PartnerLoveTypeCategory partnerLoveTypeCategory) { - if (partnerMbti != null) { - this.partnerMbti = normalizeMbti(partnerMbti); + public void updatePartnerProfile(String otherPersonalityType, PartnerLoveTypeCategory partnerLoveTypeCategory) { + if (otherPersonalityType != null) { + this.otherPersonalityType = normalizePersonalityType(otherPersonalityType); } if (partnerLoveTypeCategory != null) { this.partnerLoveTypeCategory = partnerLoveTypeCategory; @@ -234,7 +234,7 @@ public void unlinkCouple() { this.coupleId = null; } - private static String normalizeMbti(String mbti) { - return mbti == null ? null : mbti.toUpperCase(); + private static String normalizePersonalityType(String personalityType) { + return personalityType == null ? null : personalityType.toUpperCase(); } } diff --git a/src/test/java/makeus/cmc/malmo/application/service/chat/ChatPromptBuilderTest.java b/src/test/java/makeus/cmc/malmo/application/service/chat/ChatPromptBuilderTest.java index 909663c7..f7cb97a2 100644 --- a/src/test/java/makeus/cmc/malmo/application/service/chat/ChatPromptBuilderTest.java +++ b/src/test/java/makeus/cmc/malmo/application/service/chat/ChatPromptBuilderTest.java @@ -2,10 +2,10 @@ import makeus.cmc.malmo.application.helper.chat_room.ChatRoomQueryHelper; import makeus.cmc.malmo.application.helper.chat_room.MemberChatRoomMetadataQueryHelper; -import makeus.cmc.malmo.application.helper.love_type.LoveTypeMbtiPromptQueryHelper; +import makeus.cmc.malmo.application.helper.love_type.LoveTypePersonalityTypePromptQueryHelper; import makeus.cmc.malmo.application.port.out.chat.LoadChatRoomMetadataPort; import makeus.cmc.malmo.domain.model.chat.ChatRoom; -import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiPrompt; +import makeus.cmc.malmo.domain.model.love_type.LoveTypePersonalityTypePrompt; import makeus.cmc.malmo.domain.model.member.Member; import makeus.cmc.malmo.domain.value.id.InviteCodeValue; import makeus.cmc.malmo.domain.value.type.EmailForwardingStatus; @@ -44,7 +44,7 @@ class ChatPromptBuilderTest { private MemberChatRoomMetadataQueryHelper memberChatRoomMetadataQueryHelper; @Mock - private LoveTypeMbtiPromptQueryHelper loveTypeMbtiPromptQueryHelper; + private LoveTypePersonalityTypePromptQueryHelper loveTypePersonalityTypePromptQueryHelper; @InjectMocks private ChatPromptBuilder chatPromptBuilder; @@ -57,10 +57,10 @@ void createForProcessUserMessage_includesUserAndPartnerPrompts() { ChatRoom chatRoom = createChatRoom(1L); stubCommon(chatRoom, LoveTypeCategory.STABLE_TYPE, PartnerLoveTypeCategory.ANXIETY_TYPE); - when(loveTypeMbtiPromptQueryHelper.findByMbtiAndLoveTypeCategory("ISTJ", LoveTypeCategory.STABLE_TYPE)) - .thenReturn(Optional.of(LoveTypeMbtiPrompt.from("ISTJ", LoveTypeCategory.STABLE_TYPE, "ISTJ 안정형 프롬프트"))); - when(loveTypeMbtiPromptQueryHelper.findByMbtiAndLoveTypeCategory("ENFP", LoveTypeCategory.ANXIETY_TYPE)) - .thenReturn(Optional.of(LoveTypeMbtiPrompt.from("ENFP", LoveTypeCategory.ANXIETY_TYPE, "ENFP 불안형 프롬프트"))); + when(loveTypePersonalityTypePromptQueryHelper.findByPersonalityTypeAndLoveTypeCategory("ISTJ", LoveTypeCategory.STABLE_TYPE)) + .thenReturn(Optional.of(LoveTypePersonalityTypePrompt.from("ISTJ", LoveTypeCategory.STABLE_TYPE, "ISTJ 안정형 프롬프트"))); + when(loveTypePersonalityTypePromptQueryHelper.findByPersonalityTypeAndLoveTypeCategory("ENFP", LoveTypeCategory.ANXIETY_TYPE)) + .thenReturn(Optional.of(LoveTypePersonalityTypePrompt.from("ENFP", LoveTypeCategory.ANXIETY_TYPE, "ENFP 불안형 프롬프트"))); // when List> messages = chatPromptBuilder.createForProcessUserMessage(member, chatRoom, "사용자 메시지"); @@ -100,8 +100,8 @@ void createForProcessUserMessage_omitsPartnerPromptWithoutPartnerProfile() { ChatRoom chatRoom = createChatRoom(3L); stubCommon(chatRoom, LoveTypeCategory.STABLE_TYPE, null); - when(loveTypeMbtiPromptQueryHelper.findByMbtiAndLoveTypeCategory("ISTJ", LoveTypeCategory.STABLE_TYPE)) - .thenReturn(Optional.of(LoveTypeMbtiPrompt.from("ISTJ", LoveTypeCategory.STABLE_TYPE, "ISTJ 안정형 프롬프트"))); + when(loveTypePersonalityTypePromptQueryHelper.findByPersonalityTypeAndLoveTypeCategory("ISTJ", LoveTypeCategory.STABLE_TYPE)) + .thenReturn(Optional.of(LoveTypePersonalityTypePrompt.from("ISTJ", LoveTypeCategory.STABLE_TYPE, "ISTJ 안정형 프롬프트"))); // when List> messages = chatPromptBuilder.createForProcessUserMessage(member, chatRoom, "사용자 메시지"); @@ -120,7 +120,7 @@ void createForProcessUserMessage_fallsBackWhenPromptRowMissing() { ChatRoom chatRoom = createChatRoom(4L); stubCommon(chatRoom, LoveTypeCategory.STABLE_TYPE, PartnerLoveTypeCategory.ANXIETY_TYPE); - when(loveTypeMbtiPromptQueryHelper.findByMbtiAndLoveTypeCategory(any(), any())) + when(loveTypePersonalityTypePromptQueryHelper.findByPersonalityTypeAndLoveTypeCategory(any(), any())) .thenReturn(Optional.empty()); // when @@ -141,9 +141,9 @@ private void stubCommon(ChatRoom chatRoom, LoveTypeCategory userLoveType, Partne } private Member createMember( - String mbti, + String personalityType, LoveTypeCategory loveTypeCategory, - String partnerMbti, + String otherPersonalityType, PartnerLoveTypeCategory partnerLoveTypeCategory ) { LocalDateTime now = LocalDateTime.now(); @@ -167,8 +167,8 @@ private Member createMember( null, null, RelationshipStatus.IN_RELATIONSHIP, - mbti, - partnerMbti, + personalityType, + otherPersonalityType, partnerLoveTypeCategory, now, now, diff --git a/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeMbtiPromptPersistenceAdapterTest.java b/src/test/java/makeus/cmc/malmo/integration_test/LoveTypePersonalityTypePromptPersistenceAdapterTest.java similarity index 50% rename from src/test/java/makeus/cmc/malmo/integration_test/LoveTypeMbtiPromptPersistenceAdapterTest.java rename to src/test/java/makeus/cmc/malmo/integration_test/LoveTypePersonalityTypePromptPersistenceAdapterTest.java index ef1abb00..5be96601 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeMbtiPromptPersistenceAdapterTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/LoveTypePersonalityTypePromptPersistenceAdapterTest.java @@ -1,9 +1,9 @@ package makeus.cmc.malmo.integration_test; import jakarta.persistence.EntityManager; -import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypeMbtiPromptEntity; -import makeus.cmc.malmo.application.port.out.LoadLoveTypeMbtiPromptPort; -import makeus.cmc.malmo.domain.model.love_type.LoveTypeMbtiPrompt; +import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypePersonalityTypePromptEntity; +import makeus.cmc.malmo.application.port.out.LoadLoveTypePersonalityTypePromptPort; +import makeus.cmc.malmo.domain.model.love_type.LoveTypePersonalityTypePrompt; import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -15,21 +15,21 @@ @SpringBootTest @Transactional -@DisplayName("LoveTypeMbtiPromptPersistenceAdapter 테스트") -class LoveTypeMbtiPromptPersistenceAdapterTest { +@DisplayName("LoveTypePersonalityTypePromptPersistenceAdapter 테스트") +class LoveTypePersonalityTypePromptPersistenceAdapterTest { @Autowired private EntityManager em; @Autowired - private LoadLoveTypeMbtiPromptPort loadLoveTypeMbtiPromptPort; + private LoadLoveTypePersonalityTypePromptPort loadLoveTypePersonalityTypePromptPort; @Test - @DisplayName("MBTI 대소문자와 복합키 기준으로 프롬프트를 조회한다") - void loadByMbtiAndLoveTypeCategory_findsPromptIgnoringMbtiCase() { + @DisplayName("personalityType 대소문자와 복합키 기준으로 프롬프트를 조회한다") + void loadByPersonalityTypeAndLoveTypeCategory_findsPromptIgnoringPersonalityTypeCase() { // given - em.persist(LoveTypeMbtiPromptEntity.builder() - .mbti("ISTJ") + em.persist(LoveTypePersonalityTypePromptEntity.builder() + .personalityType("ISTJ") .loveTypeCategory(LoveTypeCategory.STABLE_TYPE) .prompts("ISTJ 안정형 프롬프트") .build()); @@ -37,13 +37,13 @@ void loadByMbtiAndLoveTypeCategory_findsPromptIgnoringMbtiCase() { em.clear(); // when - LoveTypeMbtiPrompt prompt = loadLoveTypeMbtiPromptPort - .loadByMbtiAndLoveTypeCategory("istj", LoveTypeCategory.STABLE_TYPE) + LoveTypePersonalityTypePrompt prompt = loadLoveTypePersonalityTypePromptPort + .loadByPersonalityTypeAndLoveTypeCategory("istj", LoveTypeCategory.STABLE_TYPE) .orElse(null); // then assertThat(prompt).isNotNull(); - assertThat(prompt.getMbti()).isEqualTo("ISTJ"); + assertThat(prompt.getPersonalityType()).isEqualTo("ISTJ"); assertThat(prompt.getLoveTypeCategory()).isEqualTo(LoveTypeCategory.STABLE_TYPE); assertThat(prompt.getPrompts()).isEqualTo("ISTJ 안정형 프롬프트"); } diff --git a/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeQuestionTest.java b/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeQuestionTest.java index eea92dda..f0fbd571 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeQuestionTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeQuestionTest.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; import jakarta.persistence.EntityManager; -import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypeMbtiFeatureEntity; +import makeus.cmc.malmo.adaptor.out.persistence.entity.LoveTypePersonalityTypeFeatureEntity; import makeus.cmc.malmo.adaptor.out.persistence.entity.TempLoveTypeEntity; import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; import makeus.cmc.malmo.integration_test.dto_factory.LoveTypeQuestionRequestDtoFactory; @@ -252,13 +252,13 @@ class GetLoveTypeResultTest { @Nested @DisplayName("MBTI + 애착 유형 상세 결과 조회 테스트") - class GetLoveTypeMbtiResultTest { + class GetLoveTypePersonalityTypeResultTest { @Test @DisplayName("MBTI와 애착 유형 상세 결과 조회 성공 - 소문자 쿼리와 빈 항목 제외") - void mbti와_애착유형_상세_결과_조회_성공() throws Exception { + void personalityType과_애착유형_상세_결과_조회_성공() throws Exception { // given - em.persist(LoveTypeMbtiFeatureEntity.builder() - .mbti("ENFP") + em.persist(LoveTypePersonalityTypeFeatureEntity.builder() + .personalityType("ENFP") .loveTypeCategory(LoveTypeCategory.STABLE_TYPE) .summary("풍부한 상상력과 사랑으로, 함께하는 일상을 즐겁게 만들어 가는 유형") .keyword1("열정적") @@ -291,13 +291,13 @@ class GetLoveTypeMbtiResultTest { .datingGuide1("감정을 정리해 표현해요") .datingGuide2("") .datingGuide3(null) - .bestMbti1("infj") + .bestPersonalityType1("infj") .bestDesc1("속마음을 깊이 이해해주며 안정적인 감정을 공유하는 궁합") - .bestMbti2("") + .bestPersonalityType2("") .bestDesc2("") - .worstMbti1("istp") + .worstPersonalityType1("istp") .worstDesc1("자유로운 감정선과 솔직한 피드백이 부딪히는 궁합") - .worstMbti2(null) + .worstPersonalityType2(null) .worstDesc2(null) .build()); em.flush(); @@ -305,11 +305,11 @@ class GetLoveTypeMbtiResultTest { // when & then mockMvc.perform(get("/love-types/result") - .param("mbti", "enfp") + .param("personalityType", "enfp") .param("lovetype", "stable_type") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andExpect(jsonPath("data.mbti").value("ENFP")) + .andExpect(jsonPath("data.personalityType").value("ENFP")) .andExpect(jsonPath("data.loveTypeCategory").value(LoveTypeCategory.STABLE_TYPE.name())) .andExpect(jsonPath("data.summary").value("풍부한 상상력과 사랑으로, 함께하는 일상을 즐겁게 만들어 가는 유형")) .andExpect(jsonPath("data.keywords.length()").value(2)) @@ -322,16 +322,16 @@ class GetLoveTypeMbtiResultTest { .andExpect(jsonPath("data.loveTypeFeatures.length()").value(1)) .andExpect(jsonPath("data.datingGuides.length()").value(1)) .andExpect(jsonPath("data.bestMatches.length()").value(1)) - .andExpect(jsonPath("data.bestMatches[0].mbti").value("INFJ")) + .andExpect(jsonPath("data.bestMatches[0].personalityType").value("INFJ")) .andExpect(jsonPath("data.worstMatches.length()").value(1)) - .andExpect(jsonPath("data.worstMatches[0].mbti").value("ISTP")); + .andExpect(jsonPath("data.worstMatches[0].personalityType").value("ISTP")); } @Test @DisplayName("MBTI와 애착 유형 상세 결과 조회 실패 - MBTI 형식 오류") - void mbti와_애착유형_상세_결과_조회_실패_mbti형식오류() throws Exception { + void personalityType과_애착유형_상세_결과_조회_실패_personalityType형식오류() throws Exception { mockMvc.perform(get("/love-types/result") - .param("mbti", "ENF") + .param("personalityType", "ENF") .param("lovetype", "STABLE_TYPE") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()) @@ -341,9 +341,9 @@ class GetLoveTypeMbtiResultTest { @Test @DisplayName("MBTI와 애착 유형 상세 결과 조회 실패 - 애착 유형 값 오류") - void mbti와_애착유형_상세_결과_조회_실패_애착유형값오류() throws Exception { + void personalityType과_애착유형_상세_결과_조회_실패_애착유형값오류() throws Exception { mockMvc.perform(get("/love-types/result") - .param("mbti", "ENFP") + .param("personalityType", "ENFP") .param("lovetype", "WRONG_TYPE") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()) @@ -353,14 +353,14 @@ class GetLoveTypeMbtiResultTest { @Test @DisplayName("MBTI와 애착 유형 상세 결과 조회 실패 - 매칭되는 결과 없음") - void mbti와_애착유형_상세_결과_조회_실패_결과없음() throws Exception { + void personalityType과_애착유형_상세_결과_조회_실패_결과없음() throws Exception { mockMvc.perform(get("/love-types/result") - .param("mbti", "ENFP") + .param("personalityType", "ENFP") .param("lovetype", "STABLE_TYPE") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()) - .andExpect(jsonPath("message").value(NO_SUCH_LOVE_TYPE_MBTI_RESULT.getMessage())) - .andExpect(jsonPath("code").value(NO_SUCH_LOVE_TYPE_MBTI_RESULT.getCode())); + .andExpect(jsonPath("message").value(NO_SUCH_LOVE_TYPE_PERSONALITY_TYPE_RESULT.getMessage())) + .andExpect(jsonPath("code").value(NO_SUCH_LOVE_TYPE_PERSONALITY_TYPE_RESULT.getCode())); } } } 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 b94c7320..99fa1634 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java @@ -826,14 +826,14 @@ public static class MemberResponseDto { String email; String relationshipStatus; - String mbti; - String partnerMbti; + String personalityType; + String otherPersonalityType; String partnerLoveTypeCategory; } @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) public static class PartnerResponseDto { - private String mbti; + private String personalityType; private String loveTypeCategory; private String description; } @@ -857,8 +857,8 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc Assertions.assertThat(memberResponse.anxietyRate).isEqualTo(member.getAnxietyRate()); Assertions.assertThat(memberResponse.nickname).isEqualTo(member.getNickname()); Assertions.assertThat(memberResponse.email).isEqualTo(member.getEmail()); - Assertions.assertThat(memberResponse.mbti).isEqualTo(member.getMbti()); - Assertions.assertThat(memberResponse.partnerMbti).isEqualTo(member.getPartnerMbti()); + Assertions.assertThat(memberResponse.personalityType).isEqualTo(member.getPersonalityType()); + Assertions.assertThat(memberResponse.otherPersonalityType).isEqualTo(member.getOtherPersonalityType()); Assertions.assertThat(memberResponse.partnerLoveTypeCategory) .isEqualTo(member.getPartnerLoveTypeCategory() == null ? null : member.getPartnerLoveTypeCategory().name()); } @@ -991,7 +991,7 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc void 파트너_멤버_정보_조회_성공() throws Exception { // given Map requestDto = Map.of( - "mbti", "enfp", + "personalityType", "enfp", "loveTypeCategory", "CONFUSION_TYPE" ); @@ -1000,7 +1000,7 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.mbti").value("ENFP")) + .andExpect(jsonPath("$.data.personalityType").value("ENFP")) .andExpect(jsonPath("$.data.loveTypeCategory").value("CONFUSION_TYPE")) .andExpect(jsonPath("$.data.description").value("혼란형")); @@ -1020,7 +1020,7 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc // 상대 프로필이 정상적으로 조회되는지 검증 PartnerResponseDto partnerDto = responseDto.data; - Assertions.assertThat(partnerDto.mbti).isEqualTo("ENFP"); + Assertions.assertThat(partnerDto.personalityType).isEqualTo("ENFP"); Assertions.assertThat(partnerDto.loveTypeCategory).isEqualTo("CONFUSION_TYPE"); Assertions.assertThat(partnerDto.description).isEqualTo("혼란형"); } @@ -1032,7 +1032,7 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc mockMvc.perform(post("/members/partners") .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(Map.of("mbti", "enfp")))) + .content(objectMapper.writeValueAsString(Map.of("personalityType", "enfp")))) .andExpect(status().isOk()); mockMvc.perform(patch("/members/partners") @@ -1060,7 +1060,7 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc ); PartnerResponseDto partnerDto = responseDto.data; - Assertions.assertThat(partnerDto.mbti).isEqualTo("ENFP"); + Assertions.assertThat(partnerDto.personalityType).isEqualTo("ENFP"); Assertions.assertThat(partnerDto.loveTypeCategory).isEqualTo("UNKNOWN"); Assertions.assertThat(partnerDto.description).isEqualTo("모르겠어요"); } @@ -1084,13 +1084,13 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc mockMvc.perform(post("/members/partners") .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(Map.of("mbti", "ENFP")))) + .content(objectMapper.writeValueAsString(Map.of("personalityType", "ENFP")))) .andExpect(status().isOk()); mockMvc.perform(post("/members/partners") .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(Map.of("mbti", "INTJ")))) + .content(objectMapper.writeValueAsString(Map.of("personalityType", "INTJ")))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("message").value(PARTNER_PROFILE_ALREADY_EXISTS.getMessage())) .andExpect(jsonPath("code").value(PARTNER_PROFILE_ALREADY_EXISTS.getCode())); @@ -1103,18 +1103,18 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) .content(""" - {"relationshipStatus":"IN_RELATIONSHIP","mbti":"intj","loveTypeCategory":"STABLE_TYPE"} + {"relationshipStatus":"IN_RELATIONSHIP","personalityType":"intj","loveTypeCategory":"STABLE_TYPE"} """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.relationshipStatus").value(RelationshipStatus.IN_RELATIONSHIP.name())) - .andExpect(jsonPath("$.data.mbti").value("INTJ")) + .andExpect(jsonPath("$.data.personalityType").value("INTJ")) .andExpect(jsonPath("$.data.loveTypeCategory").value(LoveTypeCategory.STABLE_TYPE.name())); em.flush(); em.clear(); MemberEntity updatedMember = em.find(MemberEntity.class, member.getId()); - Assertions.assertThat(updatedMember.getMbti()).isEqualTo("INTJ"); + Assertions.assertThat(updatedMember.getPersonalityType()).isEqualTo("INTJ"); Assertions.assertThat(updatedMember.getLoveTypeCategory()).isEqualTo(LoveTypeCategory.STABLE_TYPE); } diff --git a/src/test/java/makeus/cmc/malmo/mapper/MemberMapperTest.java b/src/test/java/makeus/cmc/malmo/mapper/MemberMapperTest.java index ac9fee3f..ebe9a0f5 100644 --- a/src/test/java/makeus/cmc/malmo/mapper/MemberMapperTest.java +++ b/src/test/java/makeus/cmc/malmo/mapper/MemberMapperTest.java @@ -85,8 +85,8 @@ private void assertMember(Member domain, MemberEntity entity) { assertThat(domain.getStartLoveDate()).isEqualTo(entity.getStartLoveDate()); assertThat(domain.getOauthToken()).isEqualTo(entity.getOauthToken()); assertThat(domain.getCoupleId().getValue()).isEqualTo(entity.getCoupleEntityId().getValue()); - assertThat(domain.getMbti()).isEqualTo(entity.getMbti()); - assertThat(domain.getPartnerMbti()).isEqualTo(entity.getPartnerMbti()); + assertThat(domain.getPersonalityType()).isEqualTo(entity.getPersonalityType()); + assertThat(domain.getOtherPersonalityType()).isEqualTo(entity.getOtherPersonalityType()); assertThat(domain.getPartnerLoveTypeCategory()).isEqualTo(entity.getPartnerLoveTypeCategory()); assertThat(domain.getCreatedAt()).isEqualTo(entity.getCreatedAt()); assertThat(domain.getModifiedAt()).isEqualTo(entity.getModifiedAt()); @@ -111,8 +111,8 @@ private void assertMemberEntity(MemberEntity entity, Member domain) { assertThat(entity.getStartLoveDate()).isEqualTo(domain.getStartLoveDate()); assertThat(entity.getOauthToken()).isEqualTo(domain.getOauthToken()); assertThat(entity.getCoupleEntityId().getValue()).isEqualTo(domain.getCoupleId().getValue()); - assertThat(entity.getMbti()).isEqualTo(domain.getMbti()); - assertThat(entity.getPartnerMbti()).isEqualTo(domain.getPartnerMbti()); + assertThat(entity.getPersonalityType()).isEqualTo(domain.getPersonalityType()); + assertThat(entity.getOtherPersonalityType()).isEqualTo(domain.getOtherPersonalityType()); assertThat(entity.getPartnerLoveTypeCategory()).isEqualTo(domain.getPartnerLoveTypeCategory()); assertThat(entity.getCreatedAt()).isEqualTo(domain.getCreatedAt()); assertThat(entity.getModifiedAt()).isEqualTo(domain.getModifiedAt()); @@ -139,8 +139,8 @@ private MemberEntity createCompleteEntity() { .startLoveDate(LocalDate.now()) .oauthToken("oauth_token") .coupleEntityId(CoupleEntityId.of(100L)) - .mbti("INTJ") - .partnerMbti("ENFP") + .personalityType("INTJ") + .otherPersonalityType("ENFP") .partnerLoveTypeCategory(PartnerLoveTypeCategory.UNKNOWN) .createdAt(now) .modifiedAt(now) diff --git a/src/test/java/makeus/cmc/malmo/service/AppleNotificationServiceTest.java b/src/test/java/makeus/cmc/malmo/service/AppleNotificationServiceTest.java index f9b2dd2f..1960e569 100644 --- a/src/test/java/makeus/cmc/malmo/service/AppleNotificationServiceTest.java +++ b/src/test/java/makeus/cmc/malmo/service/AppleNotificationServiceTest.java @@ -91,8 +91,8 @@ private Member createTestMember(String providerId, String oauthToken) { oauthToken, null, null, // relationshipStatus - null, // mbti - null, // partnerMbti + null, // personalityType + null, // otherPersonalityType null, // partnerLoveTypeCategory null, null, From c71c60aaa8eae66bfac469dc2faeae31bdd7152f Mon Sep 17 00:00:00 2001 From: Choi wontak <76509639+RoundTable02@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:57:53 +0900 Subject: [PATCH 06/15] =?UTF-8?q?fix:=20=EC=95=A0=EC=B0=A9=EC=9C=A0?= =?UTF-8?q?=ED=98=95=20=EC=A0=95=EB=B3=B4=20=EC=9D=91=EB=8B=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...PI-CHANGES-LOVETYPE-PERSONALITY-TYPE-RESULT.md | 2 +- .../adaptor/in/web/docs/SwaggerResponses.java | 2 +- .../GetLoveTypePersonalityTypeResultUseCase.java | 2 +- .../LoveTypePersonalityTypeFeatureService.java | 15 ++++++++++++++- .../integration_test/LoveTypeQuestionTest.java | 4 +++- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/API-CHANGES-LOVETYPE-PERSONALITY-TYPE-RESULT.md b/docs/API-CHANGES-LOVETYPE-PERSONALITY-TYPE-RESULT.md index 926de09b..1743bc65 100644 --- a/docs/API-CHANGES-LOVETYPE-PERSONALITY-TYPE-RESULT.md +++ b/docs/API-CHANGES-LOVETYPE-PERSONALITY-TYPE-RESULT.md @@ -32,7 +32,7 @@ GET /love-types/result?personalityType=enfp&lovetype=stable_type weaknesses: Array<{ title: string | null, description: string | null }>, patterns: Array<{ title: string | null, description: string | null }>, loveTypeFeatures: Array<{ title: string | null, description: string | null }>, - datingGuides: string[], + datingGuides: Array<{ title: string, description: string | null }>, bestMatches: Array<{ personalityType: string | null, description: string | null }>, worstMatches: Array<{ personalityType: string | null, description: string | null }> } 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 0dadb1e5..6cc3e67a 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 @@ -444,7 +444,7 @@ public static class LoveTypePersonalityTypeResultData { private List loveTypeFeatures; @Schema(description = "연애 가이드 목록") - private List datingGuides; + private List datingGuides; @Schema(description = "잘 맞는 MBTI 목록") private List bestMatches; diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/GetLoveTypePersonalityTypeResultUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/GetLoveTypePersonalityTypeResultUseCase.java index 1ef4e8bb..efab5a15 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/in/GetLoveTypePersonalityTypeResultUseCase.java +++ b/src/main/java/makeus/cmc/malmo/application/port/in/GetLoveTypePersonalityTypeResultUseCase.java @@ -28,7 +28,7 @@ class LoveTypePersonalityTypeResultResponse { private List weaknesses; private List patterns; private List loveTypeFeatures; - private List datingGuides; + private List datingGuides; private List bestMatches; private List worstMatches; } diff --git a/src/main/java/makeus/cmc/malmo/application/service/LoveTypePersonalityTypeFeatureService.java b/src/main/java/makeus/cmc/malmo/application/service/LoveTypePersonalityTypeFeatureService.java index aa76548b..dfa5c9d5 100644 --- a/src/main/java/makeus/cmc/malmo/application/service/LoveTypePersonalityTypeFeatureService.java +++ b/src/main/java/makeus/cmc/malmo/application/service/LoveTypePersonalityTypeFeatureService.java @@ -46,7 +46,7 @@ public LoveTypePersonalityTypeResultResponse getResult(GetLoveTypePersonalityTyp feature.getLoveTypeFeatureTitle3(), feature.getLoveTypeFeature3(), feature.getLoveTypeFeatureTitle4(), feature.getLoveTypeFeature4() )) - .datingGuides(buildStringList( + .datingGuides(buildDatingGuideItems( feature.getDatingGuide1(), feature.getDatingGuide2(), feature.getDatingGuide3() @@ -68,6 +68,19 @@ private List buildStringList(String... values) { .toList(); } + private List buildDatingGuideItems(String... values) { + return Stream.of(values) + .filter(StringUtils::hasText) + .map(value -> { + String[] parts = value.split(":", 2); + return TitleDescriptionItem.builder() + .title(parts[0].trim()) + .description(parts.length > 1 ? parts[1].trim() : null) + .build(); + }) + .toList(); + } + private List buildTitleDescriptionItems(String... values) { return Stream.iterate(0, index -> index < values.length, index -> index + 2) .map(index -> TitleDescriptionItem.builder() diff --git a/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeQuestionTest.java b/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeQuestionTest.java index f0fbd571..703a452a 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeQuestionTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/LoveTypeQuestionTest.java @@ -288,7 +288,7 @@ class GetLoveTypePersonalityTypeResultTest { .loveTypeFeature3(null) .loveTypeFeatureTitle4(null) .loveTypeFeature4(null) - .datingGuide1("감정을 정리해 표현해요") + .datingGuide1("감정을 정리해 표현해요 : 솔직하게 말하면 관계가 깊어져요") .datingGuide2("") .datingGuide3(null) .bestPersonalityType1("infj") @@ -321,6 +321,8 @@ class GetLoveTypePersonalityTypeResultTest { .andExpect(jsonPath("data.patterns.length()").value(1)) .andExpect(jsonPath("data.loveTypeFeatures.length()").value(1)) .andExpect(jsonPath("data.datingGuides.length()").value(1)) + .andExpect(jsonPath("data.datingGuides[0].title").value("감정을 정리해 표현해요")) + .andExpect(jsonPath("data.datingGuides[0].description").value("솔직하게 말하면 관계가 깊어져요")) .andExpect(jsonPath("data.bestMatches.length()").value(1)) .andExpect(jsonPath("data.bestMatches[0].personalityType").value("INFJ")) .andExpect(jsonPath("data.worstMatches.length()").value(1)) From 7e866482eb4ff5028b88535f4b002bc94a8337e2 Mon Sep 17 00:00:00 2001 From: Choi wontak <76509639+RoundTable02@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:30:17 +0900 Subject: [PATCH 07/15] =?UTF-8?q?feat:=20=EC=83=81=EB=8C=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EB=B3=80=EA=B2=BD=20API=20=EB=8B=A8?= =?UTF-8?q?=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/API-CHANGES-LOVETYPE-DATA.md | 7 +++-- .../in/web/controller/MemberController.java | 31 +------------------ .../member/UpdatePartnerProfileUseCase.java | 2 -- .../service/member/MemberCommandService.java | 8 ++--- .../cmc/malmo/domain/model/member/Member.java | 8 ++--- .../MemberIntegrationTest.java | 7 +++-- 6 files changed, 14 insertions(+), 49 deletions(-) diff --git a/docs/API-CHANGES-LOVETYPE-DATA.md b/docs/API-CHANGES-LOVETYPE-DATA.md index 71ec1d05..3ae405f4 100644 --- a/docs/API-CHANGES-LOVETYPE-DATA.md +++ b/docs/API-CHANGES-LOVETYPE-DATA.md @@ -18,11 +18,14 @@ ``` --- ### `PATCH /members/partners` — 상대방 프로필 수정 +두 필드를 항상 함께 전송해야 하며, 전달된 값으로 기존 값을 덮어씁니다. + **Request** ```ts { - personalityType?: string, - loveTypeCategory?: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE' | null + personalityType: string, // 영문 4자리 (예: "INTJ") + loveTypeCategory: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE' | null + // null = "모르겠어요" 선택 } ``` **Response** 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 5f5fdb83..4d6c4830 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 @@ -160,9 +160,7 @@ public BaseResponse updat UpdatePartnerProfileUseCase.UpdatePartnerProfileCommand.builder() .memberId(Long.valueOf(user.getUsername())) .personalityType(normalizePersonalityType(requestDto.getPersonalityType())) - .personalityTypeProvided(requestDto.isPersonalityTypeProvided()) .loveTypeCategory(requestDto.getLoveTypeCategory()) - .loveTypeCategoryProvided(requestDto.isLoveTypeCategoryProvided()) .build(); return BaseResponse.success(updatePartnerProfileUseCase.updatePartnerProfile(command)); @@ -330,38 +328,11 @@ public static class CreatePartnerProfileRequestDto { private PartnerLoveTypeCategory loveTypeCategory; } + @Data public static class UpdatePartnerProfileRequestDto { @Pattern(regexp = "^[a-zA-Z]{4}$", message = "MBTI는 영문 4자리여야 합니다.") private String personalityType; - private boolean personalityTypeProvided; private PartnerLoveTypeCategory loveTypeCategory; - private boolean loveTypeCategoryProvided; - - public String getPersonalityType() { - return personalityType; - } - - public boolean isPersonalityTypeProvided() { - return personalityTypeProvided; - } - - public PartnerLoveTypeCategory getLoveTypeCategory() { - return loveTypeCategory; - } - - public boolean isLoveTypeCategoryProvided() { - return loveTypeCategoryProvided; - } - - public void setPersonalityType(String personalityType) { - this.personalityType = personalityType; - this.personalityTypeProvided = true; - } - - public void setLoveTypeCategory(PartnerLoveTypeCategory loveTypeCategory) { - this.loveTypeCategory = loveTypeCategory; - this.loveTypeCategoryProvided = true; - } } @Data diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdatePartnerProfileUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdatePartnerProfileUseCase.java index 5f55f76f..d562b0d3 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdatePartnerProfileUseCase.java +++ b/src/main/java/makeus/cmc/malmo/application/port/in/member/UpdatePartnerProfileUseCase.java @@ -13,8 +13,6 @@ public interface UpdatePartnerProfileUseCase { class UpdatePartnerProfileCommand { private Long memberId; private String personalityType; - private boolean personalityTypeProvided; private PartnerLoveTypeCategory loveTypeCategory; - private boolean loveTypeCategoryProvided; } } 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 fcca26e3..9e2f47f3 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 @@ -100,13 +100,9 @@ public PartnerProfileResponseDto updatePartnerProfile(UpdatePartnerProfileComman throw new PartnerProfileNotFoundException("등록된 상대 프로필이 없습니다."); } - String otherPersonalityType = command.isPersonalityTypeProvided() ? command.getPersonalityType() : null; - PartnerLoveTypeCategory partnerLoveTypeCategory = null; - if (command.isLoveTypeCategoryProvided()) { - partnerLoveTypeCategory = resolvePartnerLoveTypeCategory(command.getLoveTypeCategory()); - } + PartnerLoveTypeCategory partnerLoveTypeCategory = resolvePartnerLoveTypeCategory(command.getLoveTypeCategory()); - member.updatePartnerProfile(otherPersonalityType, partnerLoveTypeCategory); + member.updatePartnerProfile(command.getPersonalityType(), partnerLoveTypeCategory); Member savedMember = memberCommandHelper.saveMember(member); return toPartnerProfileResponse(savedMember); 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 50bcb7f8..b7d328c6 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 @@ -168,12 +168,8 @@ public void createPartnerProfile(String otherPersonalityType, PartnerLoveTypeCat } public void updatePartnerProfile(String otherPersonalityType, PartnerLoveTypeCategory partnerLoveTypeCategory) { - if (otherPersonalityType != null) { - this.otherPersonalityType = normalizePersonalityType(otherPersonalityType); - } - if (partnerLoveTypeCategory != null) { - this.partnerLoveTypeCategory = partnerLoveTypeCategory; - } + this.otherPersonalityType = normalizePersonalityType(otherPersonalityType); + this.partnerLoveTypeCategory = partnerLoveTypeCategory; } public void updateLoveType(LoveTypeCategory loveTypeCategory, float avoidanceRate, float anxietyRate) { 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 99fa1634..04036999 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java @@ -1027,7 +1027,7 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc @Test @DisplayName("상대 프로필 수정 성공") - void 디데이_변경_후_파트너_정보_조회_시_isStartLoveDateUpdated_확인() throws Exception { + void 상대_프로필_수정_성공() throws Exception { // given mockMvc.perform(post("/members/partners") .header("Authorization", "Bearer " + accessToken) @@ -1039,9 +1039,10 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) .content(""" - {"loveTypeCategory":null} + {"personalityType":"intj","loveTypeCategory":null} """)) .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.personalityType").value("INTJ")) .andExpect(jsonPath("$.data.loveTypeCategory").value("UNKNOWN")) .andExpect(jsonPath("$.data.description").value("모르겠어요")); @@ -1060,7 +1061,7 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc ); PartnerResponseDto partnerDto = responseDto.data; - Assertions.assertThat(partnerDto.personalityType).isEqualTo("ENFP"); + Assertions.assertThat(partnerDto.personalityType).isEqualTo("INTJ"); Assertions.assertThat(partnerDto.loveTypeCategory).isEqualTo("UNKNOWN"); Assertions.assertThat(partnerDto.description).isEqualTo("모르겠어요"); } From 21b0172d90db0073f9e0f358233603f5c80e61c6 Mon Sep 17 00:00:00 2001 From: Choi wontak <76509639+RoundTable02@users.noreply.github.com> Date: Sat, 21 Mar 2026 13:14:02 +0900 Subject: [PATCH 08/15] =?UTF-8?q?fix:=20=EC=83=81=EB=8C=80=EB=B0=A9=20?= =?UTF-8?q?=EC=84=B1=ED=96=A5=EC=A0=95=EB=B3=B4=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EB=B0=A9=EC=8B=9D=EC=9D=84=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/API-CHANGES-ONBOARDING-PROFILE.md | 39 +++++++++++++-- .../adaptor/in/web/docs/SwaggerResponses.java | 10 ++-- .../service/member/MemberCommandService.java | 4 +- .../cmc/malmo/domain/model/member/Member.java | 8 +++- .../MemberIntegrationTest.java | 48 +++++++++++++++++++ 5 files changed, 96 insertions(+), 13 deletions(-) diff --git a/docs/API-CHANGES-ONBOARDING-PROFILE.md b/docs/API-CHANGES-ONBOARDING-PROFILE.md index c1b4582d..d1603300 100644 --- a/docs/API-CHANGES-ONBOARDING-PROFILE.md +++ b/docs/API-CHANGES-ONBOARDING-PROFILE.md @@ -95,7 +95,7 @@ "loveDay": 365, "relationshipStatus": "IN_RELATIONSHIP", "personalityType": "INTJ", - "otherPersonalityType": "ENFP" + "otherPersonalityType": "INTJ" } ``` @@ -124,7 +124,7 @@ { "nickname": "새닉네임", "relationshipStatus": "SEEING_SOMEONE", - "personalityType": "ENFP", + "personalityType": "INTJ", "otherPersonalityType": "INTJ" } ``` @@ -150,13 +150,46 @@ { "nickname": "새닉네임", "relationshipStatus": "SEEING_SOMEONE", - "personalityType": "ENFP", + "personalityType": "INTJ", "otherPersonalityType": "INTJ" } ``` --- +### 4. PATCH /members/partners (상대 프로필 수정) + +**변경 전:** `personalityType`, `loveTypeCategory` 모두 필수 + +**변경 후:** 두 필드 모두 선택 사항 — 전달된 필드만 업데이트 (부분 업데이트 지원) + +**Request:** +```json +{ + "personalityType": "INTJ", + "loveTypeCategory": "STABLE_TYPE" +} +``` + +| 필드 | 타입 | 필수 여부 | 설명 | +|------|------|-----------|------| +| `personalityType` | String | 선택 | 상대방 MBTI (영문 4자리, 대소문자 무관) | +| `loveTypeCategory` | Enum | 선택 | 상대방 애착 유형 | + +> **Note:** 필드를 생략하거나 `null`로 전달하면 해당 필드는 기존 값을 유지합니다. + +**예시 - personalityType만 수정:** +```json +{ "personalityType": "INTJ" } +``` + +**예시 - loveTypeCategory만 수정:** +```json +{ "loveTypeCategory": "ANXIETY_TYPE" } +``` + +--- + ## 마이그레이션 가이드 ### 기존 사용자 처리 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 6cc3e67a..d8309323 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 @@ -291,7 +291,7 @@ public static class MemberData { @Schema(description = "내 MBTI", example = "INTJ") private String personalityType; - @Schema(description = "상대방 MBTI", example = "ENFP") + @Schema(description = "상대방 MBTI", example = "INTJ") private String otherPersonalityType; @Schema(description = "상대방 애착 유형", example = "UNKNOWN") @@ -302,7 +302,7 @@ public static class MemberData { @Deprecated @Schema(description = "[Deprecated] 상대 프로필 조회 응답 데이터") public static class PartnerMemberData { - @Schema(description = "상대방 MBTI", example = "ENFP") + @Schema(description = "상대방 MBTI", example = "INTJ") private String personalityType; @Schema(description = "상대방 애착 유형", example = "UNKNOWN") @@ -315,7 +315,7 @@ public static class PartnerMemberData { @Getter @Schema(description = "상대 프로필 응답 데이터") public static class PartnerProfileData { - @Schema(description = "상대방 MBTI", example = "ENFP") + @Schema(description = "상대방 MBTI", example = "INTJ") private String personalityType; @Schema(description = "상대방 애착 유형", example = "UNKNOWN") @@ -419,7 +419,7 @@ public static class LoveTypeQuestionCalculationData { @Getter @Schema(description = "MBTI + 애착유형 상세 결과 응답 데이터") public static class LoveTypePersonalityTypeResultData { - @Schema(description = "personalityType", example = "ENFP") + @Schema(description = "personalityType", example = "INTJ") private String personalityType; @Schema(description = "애착 유형", example = "STABLE_TYPE") @@ -466,7 +466,7 @@ public static class LoveTypeTextBlockData { @Getter @Schema(description = "MBTI + 설명 블록") public static class LoveTypePersonalityTypeBlockData { - @Schema(description = "personalityType", example = "INFJ") + @Schema(description = "personalityType", example = "INTJ") private String personalityType; @Schema(description = "설명", example = "속마음을 깊이 이해해주며 안정적인 감정을 공유하는 궁합") 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 9e2f47f3..6be7992b 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 @@ -100,9 +100,7 @@ public PartnerProfileResponseDto updatePartnerProfile(UpdatePartnerProfileComman throw new PartnerProfileNotFoundException("등록된 상대 프로필이 없습니다."); } - PartnerLoveTypeCategory partnerLoveTypeCategory = resolvePartnerLoveTypeCategory(command.getLoveTypeCategory()); - - member.updatePartnerProfile(command.getPersonalityType(), partnerLoveTypeCategory); + member.updatePartnerProfile(command.getPersonalityType(), command.getLoveTypeCategory()); Member savedMember = memberCommandHelper.saveMember(member); return toPartnerProfileResponse(savedMember); 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 b7d328c6..50bcb7f8 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 @@ -168,8 +168,12 @@ public void createPartnerProfile(String otherPersonalityType, PartnerLoveTypeCat } public void updatePartnerProfile(String otherPersonalityType, PartnerLoveTypeCategory partnerLoveTypeCategory) { - this.otherPersonalityType = normalizePersonalityType(otherPersonalityType); - this.partnerLoveTypeCategory = partnerLoveTypeCategory; + if (otherPersonalityType != null) { + this.otherPersonalityType = normalizePersonalityType(otherPersonalityType); + } + if (partnerLoveTypeCategory != null) { + this.partnerLoveTypeCategory = partnerLoveTypeCategory; + } } public void updateLoveType(LoveTypeCategory loveTypeCategory, float avoidanceRate, float anxietyRate) { 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 04036999..71ac057f 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java @@ -1066,6 +1066,54 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc Assertions.assertThat(partnerDto.description).isEqualTo("모르겠어요"); } + @Test + @DisplayName("상대 프로필 수정 성공 - personalityType만 전달 시 loveTypeCategory 유지") + void 상대_프로필_수정_성공_personalityType만_업데이트() throws Exception { + // given - personalityType + loveTypeCategory 모두 등록 + mockMvc.perform(post("/members/partners") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"personalityType":"enfp","loveTypeCategory":"CONFUSION_TYPE"} + """)) + .andExpect(status().isOk()); + + // when - personalityType만 수정 (loveTypeCategory 미포함) + mockMvc.perform(patch("/members/partners") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"personalityType":"intj"} + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.personalityType").value("INTJ")) + .andExpect(jsonPath("$.data.loveTypeCategory").value("CONFUSION_TYPE")); + } + + @Test + @DisplayName("상대 프로필 수정 성공 - loveTypeCategory만 전달 시 personalityType 유지") + void 상대_프로필_수정_성공_loveTypeCategory만_업데이트() throws Exception { + // given - personalityType + loveTypeCategory 모두 등록 + mockMvc.perform(post("/members/partners") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"personalityType":"enfp","loveTypeCategory":"CONFUSION_TYPE"} + """)) + .andExpect(status().isOk()); + + // when - loveTypeCategory만 수정 (personalityType 미포함) + mockMvc.perform(patch("/members/partners") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"loveTypeCategory":"STABLE_TYPE"} + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.personalityType").value("ENFP")) + .andExpect(jsonPath("$.data.loveTypeCategory").value("STABLE_TYPE")); + } + @Test @DisplayName("상대 프로필 조회 실패 - 미등록 상태") void 파트너_멤버_정보_조회_실패_커플이_아닌_경우() throws Exception { From 0341de109bf3e19c58f5703c86bd6eb7e5012af4 Mon Sep 17 00:00:00 2001 From: Choi wontak <76509639+RoundTable02@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:31:04 +0900 Subject: [PATCH 09/15] =?UTF-8?q?feat:=20gemini=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=83=81=ED=99=A9=EB=B3=84=20?= =?UTF-8?q?=EB=8B=A4=EB=A5=B8=20effort=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-test.yml | 1 + .github/workflows/deploy.yml | 1 + README.md | 44 +++++ docker-compose.yml | 1 + .../AbstractOpenAiCompatibleApiClient.java | 182 ++++++++++++++++++ .../malmo/adaptor/out/GeminiApiClient.java | 83 ++++++++ .../malmo/adaptor/out/OpenAiApiClient.java | 173 +++++------------ ...kOpenAIHealth.java => CheckLlmHealth.java} | 2 +- .../port/out/chat/LlmReasoningScenario.java | 9 + .../port/out/chat/RequestChatApiPort.java | 10 +- .../application/service/OutboxService.java | 6 +- .../service/chat/ChatMessageService.java | 9 +- .../service/chat/ChatProcessor.java | 16 +- .../cmc/malmo/config/LlmStartupLogger.java | 33 ++++ .../cmc/malmo/config/MainConfiguration.java | 21 +- .../properties/GeminiApiProperties.java | 44 +++++ .../properties/OpenAiApiProperties.java | 53 +++++ .../properties/ReasoningEffortProperties.java | 36 ++++ .../cmc/malmo/util/GlobalConstants.java | 4 - src/main/resources/application-prod.yml | 29 ++- src/main/resources/application-qa.yml | 29 ++- src/main/resources/application-test.yml | 25 +++ ...AbstractOpenAiCompatibleApiClientTest.java | 85 ++++++++ .../service/chat/ChatProcessorTest.java | 119 ++++++++++++ .../malmo/config/LlmClientSelectionTest.java | 26 +++ .../LlmConfigurationPropertiesTest.java | 48 +++++ 26 files changed, 928 insertions(+), 161 deletions(-) create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/AbstractOpenAiCompatibleApiClient.java create mode 100644 src/main/java/makeus/cmc/malmo/adaptor/out/GeminiApiClient.java rename src/main/java/makeus/cmc/malmo/application/port/out/{CheckOpenAIHealth.java => CheckLlmHealth.java} (67%) create mode 100644 src/main/java/makeus/cmc/malmo/application/port/out/chat/LlmReasoningScenario.java create mode 100644 src/main/java/makeus/cmc/malmo/config/LlmStartupLogger.java create mode 100644 src/main/java/makeus/cmc/malmo/config/properties/GeminiApiProperties.java create mode 100644 src/main/java/makeus/cmc/malmo/config/properties/OpenAiApiProperties.java create mode 100644 src/main/java/makeus/cmc/malmo/config/properties/ReasoningEffortProperties.java create mode 100644 src/test/java/makeus/cmc/malmo/adaptor/out/AbstractOpenAiCompatibleApiClientTest.java create mode 100644 src/test/java/makeus/cmc/malmo/application/service/chat/ChatProcessorTest.java create mode 100644 src/test/java/makeus/cmc/malmo/config/LlmClientSelectionTest.java create mode 100644 src/test/java/makeus/cmc/malmo/config/LlmConfigurationPropertiesTest.java diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml index b0bec0cf..1b90a266 100644 --- a/.github/workflows/deploy-test.yml +++ b/.github/workflows/deploy-test.yml @@ -148,6 +148,7 @@ jobs: - APPLE_CLIENT_ID=${{ secrets.APPLE_CLIENT_ID }} - KAKAO_ADMIN_KEY=${{ secrets.KAKAO_ADMIN_KEY }} - OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} + - GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }} - SWAGGER_SERVER_PRODUCTION_URL=${{ secrets.TEST_SWAGGER_SERVER_PRODUCTION_URL }} - SECURITY_CLIENT_URL_PRODUCTION=${{ secrets.SECURITY_CLIENT_URL_PRODUCTION }} - SECURITY_CLIENT_URL_DEVELOPMENT=${{ secrets.SECURITY_CLIENT_URL_DEVELOPMENT }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b683096a..41dd1c5f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -169,6 +169,7 @@ jobs: - APPLE_CLIENT_ID=${{ secrets.APPLE_CLIENT_ID }} - KAKAO_ADMIN_KEY=${{ secrets.KAKAO_ADMIN_KEY }} - OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} + - GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }} - SWAGGER_SERVER_PRODUCTION_URL=${{ secrets.SWAGGER_SERVER_PRODUCTION_URL }} - SECURITY_CLIENT_URL_PRODUCTION=${{ secrets.SECURITY_CLIENT_URL_PRODUCTION }} - SECURITY_CLIENT_URL_DEVELOPMENT=${{ secrets.SECURITY_CLIENT_URL_DEVELOPMENT }} diff --git a/README.md b/README.md index 9058b0e2..b890d97a 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,50 @@ MZ세대는 연인과의 갈등 원인으로 '의사소통 방식'과 '성향 +--- + +## LLM / CI-CD 설정 + +- 현재 기본 LLM 구현체는 OpenAI client이며, 기본 모델은 `gpt-5.4-mini`입니다. +- Gemini 구현체도 코드에 유지되며, 필요 시 `@Primary`를 이동해 전환할 수 있습니다. +- QA/Prod 배포 workflow는 API Key, Base URL, Model 같은 민감/배포 환경별 값만 환경변수로 주입합니다. +- `GEMINI_API_KEY` +- `GEMINI_MODEL` +- `GEMINI_BASE_URL` +- `OPENAI_API_KEY` +- `OPENAI_MODEL` +- `OPENAI_BASE_URL` +- `OPENAI_STATUS_URL` +- `reasoning-effort`는 환경변수가 아니라 profile YML에 직접 선언합니다. +- 권장 YML 예시 +```yml +openai: + api: + reasoning-effort: + default: medium + scenarios: + structured-chat: low + free-conversation: low + validation: none + summary: none + auxiliary-extraction: none + +gemini: + api: + reasoning-effort: + default: high + scenarios: + structured-chat: medium + free-conversation: low + validation: low + summary: low + auxiliary-extraction: low +``` +- 롤백 절차 +- OpenAI 구현체에 `@Primary`를 옮깁니다. +- 필요 시 `OPENAI_MODEL`만 조정합니다. +- 재배포합니다. reasoning effort 조정은 `application-*.yml` 수정으로 처리합니다. + --- ## 🚎 Architecture diff --git a/docker-compose.yml b/docker-compose.yml index f9d790e3..d91b55f8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,7 @@ services: - KAKAO_REST_API_KEY=${KAKAO_REST_API_KEY} - APPLE_REST_API_KEY=${APPLE_REST_API_KEY} - OPENAI_API_KEY=${OPENAI_API_KEY} + - GEMINI_API_KEY=${GEMINI_API_KEY} - SWAGGER_SERVER_PRODUCTION_URL=${SWAGGER_SERVER_PRODUCTION_URL} - SECURITY_CLIENT_URL_PRODUCTION=${SECURITY_CLIENT_URL_PRODUCTION} - SECURITY_CLIENT_URL_DEVELOPMENT=${SECURITY_CLIENT_URL_DEVELOPMENT} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/AbstractOpenAiCompatibleApiClient.java b/src/main/java/makeus/cmc/malmo/adaptor/out/AbstractOpenAiCompatibleApiClient.java new file mode 100644 index 00000000..fc7dfa99 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/AbstractOpenAiCompatibleApiClient.java @@ -0,0 +1,182 @@ +package makeus.cmc.malmo.adaptor.out; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import makeus.cmc.malmo.application.port.out.chat.LlmReasoningScenario; +import makeus.cmc.malmo.application.port.out.chat.RequestChatApiPort; +import makeus.cmc.malmo.config.properties.ReasoningEffortProperties; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +@Slf4j +public abstract class AbstractOpenAiCompatibleApiClient implements RequestChatApiPort { + + protected final WebClient webClient; + protected final ObjectMapper objectMapper; + + protected AbstractOpenAiCompatibleApiClient(WebClient webClient, ObjectMapper objectMapper) { + this.webClient = webClient; + this.objectMapper = objectMapper; + } + + protected abstract String getProviderName(); + + protected abstract String getApiKey(); + + protected abstract String getModel(); + + protected abstract ReasoningEffortProperties getReasoningEffortProperties(); + + public final String currentProviderName() { + return getProviderName(); + } + + public final String currentModel() { + return getModel(); + } + + public final String currentDefaultReasoningEffort() { + ReasoningEffortProperties properties = getReasoningEffortProperties(); + return properties != null ? properties.getDefault() : null; + } + + public final Map currentScenarioReasoningEfforts() { + ReasoningEffortProperties properties = getReasoningEffortProperties(); + if (properties == null || properties.getScenarios() == null) { + return Map.of(); + } + return Map.copyOf(new LinkedHashMap<>(properties.getScenarios())); + } + + public final String currentReasoningEffort(LlmReasoningScenario scenario) { + return resolveReasoningEffort(scenario); + } + + @Override + public Mono requestStreamResponse(List> messages, + LlmReasoningScenario scenario, + Consumer onData) { + Map body = createBody(messages, scenario, true, false); + + return webClient.post() + .uri("/chat/completions") + .header("Authorization", "Bearer " + getApiKey()) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(body) + .retrieve() + .onStatus(HttpStatusCode::isError, response -> + response.bodyToMono(String.class) + .flatMap(errorBody -> { + log.error("{} API error response: {}", getProviderName(), errorBody); + return Mono.error(new RuntimeException(getProviderName() + " API error: " + errorBody)); + }) + ) + .bodyToFlux(String.class) + .filter(line -> !line.isBlank()) + .takeWhile(data -> !data.equals("[DONE]") && !data.equals("data: [DONE]")) + .map(data -> { + try { + return extractStreamContent(data); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to parse stream content", e); + } + }) + .filter(content -> !content.isEmpty()) + .doOnNext(onData) + .collect(Collectors.joining("")) + .doOnError(throwable -> log.error("Error during {} stream processing", getProviderName(), throwable)); + } + + @Override + public CompletableFuture requestResponse(List> messages, + LlmReasoningScenario scenario) { + return sendRequest(createBody(messages, scenario, false, false)) + .thenApply(this::extractContent); + } + + @Override + public CompletableFuture requestJsonResponse(List> messages, + LlmReasoningScenario scenario) { + return sendRequest(createBody(messages, scenario, false, true)) + .thenApply(this::extractContent); + } + + private CompletableFuture sendRequest(Map body) { + return webClient.post() + .uri("/chat/completions") + .header("Authorization", "Bearer " + getApiKey()) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(body) + .retrieve() + .bodyToMono(String.class) + .toFuture(); + } + + private String extractContent(String data) { + try { + JsonNode node = objectMapper.readTree(data); + return node.path("choices").get(0).path("message").path("content").asText(); + } catch (JsonProcessingException e) { + log.error("Error processing {} API response", getProviderName(), e); + throw new RuntimeException(e); + } + } + + private String extractStreamContent(String data) throws JsonProcessingException { + String normalized = data.startsWith("data: ") ? data.substring(6) : data; + JsonNode node = objectMapper.readTree(normalized); + return node + .path("choices").get(0) + .path("delta") + .path("content") + .asText(); + } + + protected final Map createBody(List> messages, + LlmReasoningScenario scenario, + boolean stream, + boolean jsonResponse) { + Map body = new java.util.LinkedHashMap<>(); + body.put("model", getModel()); + + String reasoningEffort = resolveReasoningEffort(scenario); + if (StringUtils.hasText(reasoningEffort)) { + body.put("reasoning_effort", reasoningEffort); + } + + if (jsonResponse) { + body.put("response_format", Map.of("type", "json_object")); + } + + body.put("messages", messages); + body.put("stream", stream); + return body; + } + + private String resolveReasoningEffort(LlmReasoningScenario scenario) { + ReasoningEffortProperties properties = getReasoningEffortProperties(); + if (properties == null) { + return null; + } + + String scenarioEffort = properties.getScenarioEffort(scenario); + if (StringUtils.hasText(scenarioEffort)) { + return scenarioEffort; + } + + String defaultEffort = properties.getDefault(); + return StringUtils.hasText(defaultEffort) ? defaultEffort : null; + } +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/GeminiApiClient.java b/src/main/java/makeus/cmc/malmo/adaptor/out/GeminiApiClient.java new file mode 100644 index 00000000..48ca0dda --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/GeminiApiClient.java @@ -0,0 +1,83 @@ +package makeus.cmc.malmo.adaptor.out; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import makeus.cmc.malmo.application.port.out.CheckLlmHealth; +import makeus.cmc.malmo.config.properties.GeminiApiProperties; +import makeus.cmc.malmo.config.properties.ReasoningEffortProperties; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Primary; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; + +@Slf4j +//@Primary +@Component +public class GeminiApiClient extends AbstractOpenAiCompatibleApiClient implements CheckLlmHealth { + + private final GeminiApiProperties geminiApiProperties; + private final RestTemplate restTemplate; + + public GeminiApiClient( + @Qualifier("geminiWebClient") WebClient webClient, + ObjectMapper objectMapper, + GeminiApiProperties geminiApiProperties, + RestTemplate restTemplate + ) { + super(webClient, objectMapper); + this.geminiApiProperties = geminiApiProperties; + this.restTemplate = restTemplate; + } + + @Override + protected String getProviderName() { + return "Gemini"; + } + + @Override + protected String getApiKey() { + return geminiApiProperties.getKey(); + } + + @Override + protected String getModel() { + return geminiApiProperties.getModel(); + } + + @Override + protected ReasoningEffortProperties getReasoningEffortProperties() { + return geminiApiProperties.getReasoningEffort(); + } + + @Override + public boolean checkHealth() { + try { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(geminiApiProperties.getKey()); + + ResponseEntity response = restTemplate.exchange( + geminiApiProperties.getBaseUrl() + "/models/" + geminiApiProperties.getModel(), + HttpMethod.GET, + new HttpEntity<>(headers), + String.class + ); + + boolean isUp = response.getStatusCode().is2xxSuccessful(); + if (isUp) { + log.info("Gemini API is UP for model={}", geminiApiProperties.getModel()); + } else { + log.warn("Gemini API check returned status={} for model={}", + response.getStatusCode(), geminiApiProperties.getModel()); + } + return isUp; + } catch (Exception e) { + log.error("Gemini HealthCheck failed", e); + return false; + } + } +} 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 487d46b3..44731872 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/OpenAiApiClient.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/OpenAiApiClient.java @@ -1,177 +1,90 @@ package makeus.cmc.malmo.adaptor.out; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import makeus.cmc.malmo.adaptor.out.exception.OpenAiRequestException; -import makeus.cmc.malmo.application.port.out.CheckOpenAIHealth; -import makeus.cmc.malmo.application.port.out.chat.RequestChatApiPort; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.MediaType; +import makeus.cmc.malmo.application.port.out.CheckLlmHealth; +import makeus.cmc.malmo.config.properties.OpenAiApiProperties; +import makeus.cmc.malmo.config.properties.ReasoningEffortProperties; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Primary; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.Disposable; -import reactor.core.publisher.Mono; -import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -import static makeus.cmc.malmo.util.GlobalConstants.OPENAI_STATUS_URL; @Slf4j +@Primary @Component -@RequiredArgsConstructor -public class OpenAiApiClient implements RequestChatApiPort, CheckOpenAIHealth { - - public static final String GPT_VERSION = "gpt-4.1"; - public static final double GPT_TEMPERATURE = 0.5; - - @Value("${openai.api.key}") - private String openAiApiKey; - - private final WebClient webClient; - private final ObjectMapper objectMapper; +public class OpenAiApiClient extends AbstractOpenAiCompatibleApiClient implements CheckLlmHealth { + private final OpenAiApiProperties openAiApiProperties; private final RestTemplate restTemplate; - @Override - public Mono requestStreamResponse(List> messages, Consumer onData) { - Map body = createStreamBody(messages); - - // WebClient로 스트리밍 데이터를 Flux 형태로 요청 - return webClient.post() - .uri("/chat/completions") - .header("Authorization", "Bearer " + openAiApiKey) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(body) - .retrieve() - .onStatus(HttpStatusCode::isError, response -> - response.bodyToMono(String.class) - .flatMap(errorBody -> { - log.error("OpenAI API error response: {}", errorBody); - return Mono.error(new RuntimeException("OpenAI API error: " + errorBody)); - }) - ) - .bodyToFlux(String.class) - .filter(line -> !line.isBlank()) // 비어있는 줄 필터링은 유지 - .takeWhile(data -> !data.equals("[DONE]")) - .map(data -> { - try { - return extractStreamContent(data); - } catch (JsonProcessingException e) { - throw new RuntimeException("Failed to parse stream content", e); - } - }) - .filter(content -> !content.isEmpty()) - .doOnNext(onData) - .collect(Collectors.joining("")) - .doOnError(throwable -> log.error("Error during OpenAI stream processing", throwable)); + public OpenAiApiClient( + @Qualifier("openAiWebClient") WebClient webClient, + ObjectMapper objectMapper, + OpenAiApiProperties openAiApiProperties, + RestTemplate restTemplate + ) { + super(webClient, objectMapper); + this.openAiApiProperties = openAiApiProperties; + this.restTemplate = restTemplate; } @Override - public CompletableFuture requestResponse(List> messages) { - Map body = createBody(messages); - return sendRequest(body) - .thenApply(this::extractContent); // 응답이 오면 extractContent 실행 + protected String getProviderName() { + return "OpenAI"; } @Override - public CompletableFuture requestJsonResponse(List> messages) { - Map body = createBodyForJsonResponse(messages); - return sendRequest(body) - .thenApply(this::extractContent); // 응답이 오면 extractContent 실행 - } - - private CompletableFuture sendRequest(Map body) { - return webClient.post() - .uri("/chat/completions") - .header("Authorization", "Bearer " + openAiApiKey) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(body) - .retrieve() - .bodyToMono(String.class) // 응답 바디를 Mono으로 받음 - .toFuture(); // Mono를 CompletableFuture로 변환 - } - - private String extractContent(String data) { - try { - JsonNode node = objectMapper.readTree(data); - return node.path("choices").get(0).path("message").path("content").asText(); - } catch (JsonProcessingException e) { - log.error("Error processing OpenAI API response", e); - throw new RuntimeException(e); // 예외를 던져 CompletableFuture가 exceptionally로 처리하게 함 - } + protected String getApiKey() { + return openAiApiProperties.getKey(); } - - private String extractStreamContent(String data) throws JsonProcessingException { - JsonNode node = new ObjectMapper().readTree(data); - return node - .path("choices").get(0) - .path("delta") - .path("content") - .asText(); - } - - private Map createStreamBody(List> messages) { - return Map.of( - "model", GPT_VERSION, - "messages", messages, - "temperature", GPT_TEMPERATURE, - "stream", true - ); - } - - private Map createBody(List> messages) { - return Map.of( - "model", GPT_VERSION, - "messages", messages, - "temperature", GPT_TEMPERATURE, - "stream", false - ); + @Override + protected String getModel() { + return openAiApiProperties.getModel(); } - private Map createBodyForJsonResponse(List> messages) { - return Map.of( - "model", GPT_VERSION, - "response_format", Map.of("type", "json_object"), - "messages", messages, - "temperature", GPT_TEMPERATURE, - "stream", false - ); + @Override + protected ReasoningEffortProperties getReasoningEffortProperties() { + return openAiApiProperties.getReasoningEffort(); } @Override public boolean checkHealth() { try { - Map response = restTemplate.getForObject(OPENAI_STATUS_URL, Map.class); - if (response == null) { + ResponseEntity response = restTemplate.exchange( + openAiApiProperties.getStatusUrl(), + HttpMethod.GET, + HttpEntity.EMPTY, + Map.class + ); + + Map body = response.getBody(); + if (body == null) { log.warn("OpenAI HealthCheck: Empty response"); return false; } - Map status = (Map) response.get("status"); + Map status = (Map) body.get("status"); String indicator = (String) status.get("indicator"); String description = (String) status.get("description"); if ("none".equalsIgnoreCase(indicator)) { log.info("OpenAI API is UP: {}", description); return true; - } else { - log.warn(" OpenAI API Issue: {} ({})", description, indicator); - return false; } + + log.warn("OpenAI API Issue: {} ({})", description, indicator); + return false; } catch (Exception e) { log.error("OpenAI HealthCheck failed", e); return false; } } } - diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/CheckOpenAIHealth.java b/src/main/java/makeus/cmc/malmo/application/port/out/CheckLlmHealth.java similarity index 67% rename from src/main/java/makeus/cmc/malmo/application/port/out/CheckOpenAIHealth.java rename to src/main/java/makeus/cmc/malmo/application/port/out/CheckLlmHealth.java index d4c14420..6155aebf 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/out/CheckOpenAIHealth.java +++ b/src/main/java/makeus/cmc/malmo/application/port/out/CheckLlmHealth.java @@ -1,5 +1,5 @@ package makeus.cmc.malmo.application.port.out; -public interface CheckOpenAIHealth { +public interface CheckLlmHealth { boolean checkHealth(); } diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/chat/LlmReasoningScenario.java b/src/main/java/makeus/cmc/malmo/application/port/out/chat/LlmReasoningScenario.java new file mode 100644 index 00000000..8ff9da1f --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/port/out/chat/LlmReasoningScenario.java @@ -0,0 +1,9 @@ +package makeus.cmc.malmo.application.port.out.chat; + +public enum LlmReasoningScenario { + STRUCTURED_CHAT, + FREE_CONVERSATION, + VALIDATION, + SUMMARY, + AUXILIARY_EXTRACTION +} diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/chat/RequestChatApiPort.java b/src/main/java/makeus/cmc/malmo/application/port/out/chat/RequestChatApiPort.java index 697cd3f5..02a3005f 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/out/chat/RequestChatApiPort.java +++ b/src/main/java/makeus/cmc/malmo/application/port/out/chat/RequestChatApiPort.java @@ -8,9 +8,13 @@ import java.util.function.Consumer; public interface RequestChatApiPort { - Mono requestStreamResponse(List> messages, Consumer onData); + Mono requestStreamResponse(List> messages, + LlmReasoningScenario scenario, + Consumer onData); - CompletableFuture requestResponse(List> messages); + CompletableFuture requestResponse(List> messages, + LlmReasoningScenario scenario); - CompletableFuture requestJsonResponse(List> messages); + CompletableFuture requestJsonResponse(List> messages, + LlmReasoningScenario scenario); } diff --git a/src/main/java/makeus/cmc/malmo/application/service/OutboxService.java b/src/main/java/makeus/cmc/malmo/application/service/OutboxService.java index 93894a30..9c1beba9 100644 --- a/src/main/java/makeus/cmc/malmo/application/service/OutboxService.java +++ b/src/main/java/makeus/cmc/malmo/application/service/OutboxService.java @@ -7,7 +7,7 @@ import makeus.cmc.malmo.application.port.in.MarkOutboxUseCase; import makeus.cmc.malmo.application.port.in.PublishStreamMessageUseCase; import makeus.cmc.malmo.application.port.in.RetryPublishingUseCase; -import makeus.cmc.malmo.application.port.out.CheckOpenAIHealth; +import makeus.cmc.malmo.application.port.out.CheckLlmHealth; import makeus.cmc.malmo.application.port.out.LoadOutboxPort; import makeus.cmc.malmo.application.port.out.SaveOutboxPort; import makeus.cmc.malmo.application.port.out.chat.LoadPendingMessagePort; @@ -36,7 +36,7 @@ public class OutboxService implements PublishStreamMessageUseCase, RetryPublishi private final LoadPendingMessagePort loadPendingMessagePort; private final LoadOutboxPort loadOutboxPort; private final SaveOutboxPort saveOutboxPort; - private final CheckOpenAIHealth checkOpenAIHealth; + private final CheckLlmHealth checkLlmHealth; @Override @Transactional @@ -114,7 +114,7 @@ public void retryPublishing() { @Override @Transactional public void retryFailedMessages() { - boolean isUp = checkOpenAIHealth.checkHealth(); + boolean isUp = checkLlmHealth.checkHealth(); if (!isUp) return; List failedOutboxList = loadOutboxPort.findByState(OutboxState.FAILED); 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 5b63430a..380be34d 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 @@ -11,6 +11,7 @@ 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.out.chat.LlmReasoningScenario; import makeus.cmc.malmo.application.port.in.chat.ProcessMessageUseCase; import makeus.cmc.malmo.application.port.in.chat.SufficiencyCheckResult; import makeus.cmc.malmo.domain.model.chat.ChatMessage; @@ -177,7 +178,7 @@ private CompletableFuture processFreeConversation(Member member, ChatRoom DetailedPrompt detailedPrompt = detailedPromptQueryHelper.getGuidelinePromptWithFallback( command.getPromptLevel(), command.getDetailedLevel()); - return chatProcessor.streamChat(messages, systemPrompt, prompt, detailedPrompt, + return chatProcessor.streamChat(messages, LlmReasoningScenario.FREE_CONVERSATION, 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), @@ -212,7 +213,7 @@ private CompletableFuture requestResponseToMeetCondition(Member member, Ch DetailedPrompt detailedPrompt = detailedPromptQueryHelper.getGuidelinePromptWithFallback( command.getPromptLevel(), command.getDetailedLevel()); - return chatProcessor.streamChat(messages, systemPrompt, prompt, detailedPrompt, + return chatProcessor.streamChat(messages, LlmReasoningScenario.STRUCTURED_CHAT, 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), @@ -250,7 +251,7 @@ private CompletableFuture requestNextDetailedPromptOpening(ChatRoom chatRo DetailedPrompt nextDetailedPrompt = detailedPromptQueryHelper.getGuidelinePromptWithFallback( command.getPromptLevel(), command.getDetailedLevel() + 1); - return chatProcessor.streamChat(messages, systemPrompt, prompt, nextDetailedPrompt, + return chatProcessor.streamChat(messages, LlmReasoningScenario.STRUCTURED_CHAT, systemPrompt, prompt, nextDetailedPrompt, chunk -> chatSseSender.sendResponseChunk(memberId, chunk), fullAnswer -> saveAiMessage(memberId, ChatRoomId.of(chatRoom.getId()), command.getPromptLevel(), command.getDetailedLevel() + 1, fullAnswer), @@ -272,7 +273,7 @@ private CompletableFuture requestNextStageOpening(Member member, ChatRoom // DetailedPrompt도 fallback 로직 적용 DetailedPrompt nextDetailedPrompt = detailedPromptQueryHelper.getGuidelinePromptWithFallback(nextLevel, 1); - return chatProcessor.streamChat(messages, systemPrompt, nextPrompt, nextDetailedPrompt, + return chatProcessor.streamChat(messages, LlmReasoningScenario.STRUCTURED_CHAT, systemPrompt, nextPrompt, nextDetailedPrompt, chunk -> chatSseSender.sendResponseChunk(MemberId.of(member.getId()), chunk), fullAnswer -> saveAiMessage(MemberId.of(member.getId()), ChatRoomId.of(chatRoom.getId()), nextLevel, 1, fullAnswer), diff --git a/src/main/java/makeus/cmc/malmo/application/service/chat/ChatProcessor.java b/src/main/java/makeus/cmc/malmo/application/service/chat/ChatProcessor.java index 5305c95a..71e2417b 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 @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import makeus.cmc.malmo.application.port.out.chat.LlmReasoningScenario; 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; @@ -27,6 +28,7 @@ public class ChatProcessor { private final ObjectMapper objectMapper; public Mono streamChat(List> messages, + LlmReasoningScenario scenario, Prompt systemPrompt, Prompt prompt, DetailedPrompt detailedPrompt, @@ -40,7 +42,7 @@ public Mono streamChat(List> messages, log.info("Starting streamChat with messages: {}", messages); - return requestChatApiPort.requestStreamResponse(messages, onChunk) // onChunk 콜백만 넘김 + return requestChatApiPort.requestStreamResponse(messages, scenario, onChunk) .flatMap(fullAnswer -> { // 스트림이 성공적으로 완료되고 전체 응답(fullAnswer)이 오면 onComplete 로직 실행 onComplete.accept(fullAnswer); @@ -60,7 +62,7 @@ public CompletableFuture requestSummaryAsync(List> m messages.add(createMessageMap(SenderType.SYSTEM, prompt.getContent())); messages.add(createMessageMap(SenderType.SYSTEM, "[현재 단계 지시] " + summaryPrompt.getContent())); - return requestChatApiPort.requestResponse(messages); + return requestChatApiPort.requestResponse(messages, LlmReasoningScenario.SUMMARY); } @@ -73,7 +75,7 @@ public CompletableFuture requestMetaData(String question, createMessageMap(SenderType.USER, "[답변] " + memberAnswer) ); - return requestChatApiPort.requestResponse(messages); + return requestChatApiPort.requestResponse(messages, LlmReasoningScenario.AUXILIARY_EXTRACTION); } public CompletableFuture requestSufficiencyCheck(List> messages, @@ -82,7 +84,7 @@ public CompletableFuture requestSufficiencyCheck(List { try { log.info("Received sufficiency check JSON: {}", jsonResponse); @@ -97,7 +99,7 @@ public CompletableFuture requestSufficiencyCheck(List requestDetailedSummary(List> messages, DetailedPrompt summaryPrompt) { messages.add(createMessageMap(SenderType.SYSTEM, summaryPrompt.getContent())); - return requestChatApiPort.requestResponse(messages); + return requestChatApiPort.requestResponse(messages, LlmReasoningScenario.SUMMARY); } /** @@ -109,7 +111,7 @@ public CompletableFuture requestDetailedSummary(List public CompletableFuture requestConversationSummary(List> messages, Prompt summaryPrompt) { messages.add(createMessageMap(SenderType.SYSTEM, summaryPrompt.getContent())); - return requestChatApiPort.requestResponse(messages); + return requestChatApiPort.requestResponse(messages, LlmReasoningScenario.SUMMARY); } /** @@ -123,7 +125,7 @@ public CompletableFuture requestTitleGeneration(List List> promptMessages = new ArrayList<>(messages); promptMessages.add(createMessageMap(SenderType.SYSTEM, titlePrompt.getContent())); - return requestChatApiPort.requestResponse(promptMessages) + return requestChatApiPort.requestResponse(promptMessages, LlmReasoningScenario.AUXILIARY_EXTRACTION) .thenApply(title -> { // 제목 길이 제한 (최대 50자) String trimmedTitle = title.trim(); diff --git a/src/main/java/makeus/cmc/malmo/config/LlmStartupLogger.java b/src/main/java/makeus/cmc/malmo/config/LlmStartupLogger.java new file mode 100644 index 00000000..b2479d59 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/config/LlmStartupLogger.java @@ -0,0 +1,33 @@ +package makeus.cmc.malmo.config; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import makeus.cmc.malmo.adaptor.out.AbstractOpenAiCompatibleApiClient; +import makeus.cmc.malmo.application.port.out.chat.RequestChatApiPort; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LlmStartupLogger { + + private final RequestChatApiPort requestChatApiPort; + + @EventListener(ApplicationReadyEvent.class) + public void logActiveLlmConfiguration() { + if (requestChatApiPort instanceof AbstractOpenAiCompatibleApiClient llmClient) { + log.info( + "Active LLM configuration: provider={}, model={}, defaultReasoningEffort={}, scenarioReasoningEfforts={}", + llmClient.currentProviderName(), + llmClient.currentModel(), + llmClient.currentDefaultReasoningEffort(), + llmClient.currentScenarioReasoningEfforts() + ); + return; + } + + log.info("Active LLM configuration: clientClass={}", requestChatApiPort.getClass().getName()); + } +} diff --git a/src/main/java/makeus/cmc/malmo/config/MainConfiguration.java b/src/main/java/makeus/cmc/malmo/config/MainConfiguration.java index 28dc5f43..55c64c08 100644 --- a/src/main/java/makeus/cmc/malmo/config/MainConfiguration.java +++ b/src/main/java/makeus/cmc/malmo/config/MainConfiguration.java @@ -3,15 +3,19 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import makeus.cmc.malmo.config.properties.GeminiApiProperties; +import makeus.cmc.malmo.config.properties.OpenAiApiProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; -import static makeus.cmc.malmo.util.GlobalConstants.OPENAI_CHAT_URL; - - @Configuration +@EnableConfigurationProperties({ + OpenAiApiProperties.class, + GeminiApiProperties.class +}) public class MainConfiguration { @Bean @@ -23,9 +27,16 @@ public ObjectMapper objectMapper() { } @Bean - public WebClient webClient() { + public WebClient openAiWebClient(OpenAiApiProperties openAiApiProperties) { + return WebClient.builder() + .baseUrl(openAiApiProperties.getBaseUrl()) + .build(); + } + + @Bean + public WebClient geminiWebClient(GeminiApiProperties geminiApiProperties) { return WebClient.builder() - .baseUrl(OPENAI_CHAT_URL) + .baseUrl(geminiApiProperties.getBaseUrl()) .build(); } diff --git a/src/main/java/makeus/cmc/malmo/config/properties/GeminiApiProperties.java b/src/main/java/makeus/cmc/malmo/config/properties/GeminiApiProperties.java new file mode 100644 index 00000000..061c8f24 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/config/properties/GeminiApiProperties.java @@ -0,0 +1,44 @@ +package makeus.cmc.malmo.config.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "gemini.api") +public class GeminiApiProperties { + + private String key; + private String model; + private String baseUrl; + private ReasoningEffortProperties reasoningEffort = new ReasoningEffortProperties(); + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public String getBaseUrl() { + return baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public ReasoningEffortProperties getReasoningEffort() { + return reasoningEffort; + } + + public void setReasoningEffort(ReasoningEffortProperties reasoningEffort) { + this.reasoningEffort = reasoningEffort; + } +} diff --git a/src/main/java/makeus/cmc/malmo/config/properties/OpenAiApiProperties.java b/src/main/java/makeus/cmc/malmo/config/properties/OpenAiApiProperties.java new file mode 100644 index 00000000..03c4627e --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/config/properties/OpenAiApiProperties.java @@ -0,0 +1,53 @@ +package makeus.cmc.malmo.config.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "openai.api") +public class OpenAiApiProperties { + + private String key; + private String model; + private String baseUrl; + private String statusUrl; + private ReasoningEffortProperties reasoningEffort = new ReasoningEffortProperties(); + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public String getBaseUrl() { + return baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public String getStatusUrl() { + return statusUrl; + } + + public void setStatusUrl(String statusUrl) { + this.statusUrl = statusUrl; + } + + public ReasoningEffortProperties getReasoningEffort() { + return reasoningEffort; + } + + public void setReasoningEffort(ReasoningEffortProperties reasoningEffort) { + this.reasoningEffort = reasoningEffort; + } +} diff --git a/src/main/java/makeus/cmc/malmo/config/properties/ReasoningEffortProperties.java b/src/main/java/makeus/cmc/malmo/config/properties/ReasoningEffortProperties.java new file mode 100644 index 00000000..7b6a723c --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/config/properties/ReasoningEffortProperties.java @@ -0,0 +1,36 @@ +package makeus.cmc.malmo.config.properties; + +import makeus.cmc.malmo.application.port.out.chat.LlmReasoningScenario; + +import java.util.EnumMap; +import java.util.Map; + +public class ReasoningEffortProperties { + + private String defaultEffort; + private Map scenarios = new EnumMap<>(LlmReasoningScenario.class); + + public String getDefault() { + return defaultEffort; + } + + public void setDefault(String defaultEffort) { + this.defaultEffort = defaultEffort; + } + + public Map getScenarios() { + return scenarios; + } + + public void setScenarios(Map scenarios) { + if (scenarios == null || scenarios.isEmpty()) { + this.scenarios = new EnumMap<>(LlmReasoningScenario.class); + return; + } + this.scenarios = new EnumMap<>(scenarios); + } + + public String getScenarioEffort(LlmReasoningScenario scenario) { + return scenarios.get(scenario); + } +} diff --git a/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java b/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java index f59f6b71..e0cc110d 100644 --- a/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java +++ b/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java @@ -16,10 +16,6 @@ public class GlobalConstants { public static final String INIT_CHAT_MESSAGE_SECOND_BREAKUP = "오늘은 어떤 고민 때문에 나를 찾아왔어? 이별 전후로 마음에 남아 있는 상황을 이야기해 주면 내가 같이 고민해볼게!"; - 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 String ATTACHMENT_TYPE_PROMPT_MESSAGE = "잠깐! 애착유형 테스트를 하면, 더 정확한 상담이 가능해! 그대로 진행하면 바로 상담해줄게"; // 커플 복구 관련 상수 diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 7e392f56..555e68d4 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -63,7 +63,32 @@ swagger: openai: api: - key: ${OPENAI_API_KEY} + key: ${OPENAI_API_KEY:} + model: gpt-5.4-mini + base-url: https://api.openai.com/v1 + status-url: https://status.openai.com/api/v2/status.json + reasoning-effort: + default: medium + scenarios: + structured-chat: low + free-conversation: low + validation: none + summary: none + auxiliary-extraction: none + +gemini: + api: + key: ${GEMINI_API_KEY:} + model: gemini-3-flash-preview + base-url: https://generativelanguage.googleapis.com/v1beta/openai + reasoning-effort: + default: high + scenarios: + structured-chat: medium + free-conversation: low + validation: low + summary: low + auxiliary-extraction: low security: server: @@ -93,4 +118,4 @@ sentry: logging: minimum-event-level: ERROR send-default-pii: true - max-request-body-size: medium \ No newline at end of file + max-request-body-size: medium diff --git a/src/main/resources/application-qa.yml b/src/main/resources/application-qa.yml index 81fa90a9..a6b5d91f 100644 --- a/src/main/resources/application-qa.yml +++ b/src/main/resources/application-qa.yml @@ -63,7 +63,32 @@ swagger: openai: api: - key: ${OPENAI_API_KEY} + key: ${OPENAI_API_KEY:} + model: gpt-5.4-mini + base-url: https://api.openai.com/v1 + status-url: https://status.openai.com/api/v2/status.json + reasoning-effort: + default: medium + scenarios: + structured-chat: low + free-conversation: low + validation: none + summary: none + auxiliary-extraction: none + +gemini: + api: + key: ${GEMINI_API_KEY:} + model: gemini-3-flash-preview + base-url: https://generativelanguage.googleapis.com/v1beta/openai + reasoning-effort: + default: high + scenarios: + structured-chat: medium + free-conversation: low + validation: low + summary: low + auxiliary-extraction: low security: server: @@ -93,4 +118,4 @@ sentry: logging: minimum-event-level: ERROR send-default-pii: true - max-request-body-size: medium \ No newline at end of file + max-request-body-size: medium diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 575bf9bb..27277b7d 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -73,6 +73,31 @@ swagger: openai: api: key: sk-test-openai-api-key-for-testing-only + model: gpt-5.4-mini + base-url: https://api.openai.com/v1 + status-url: https://status.openai.com/api/v2/status.json + reasoning-effort: + default: medium + scenarios: + structured-chat: low + free-conversation: low + validation: none + summary: none + auxiliary-extraction: none + +gemini: + api: + key: test-gemini-api-key-for-testing-only + model: gemini-3-flash-preview + base-url: https://generativelanguage.googleapis.com/v1beta/openai + reasoning-effort: + default: high + scenarios: + structured-chat: medium + free-conversation: low + validation: low + summary: low + auxiliary-extraction: low security: server: diff --git a/src/test/java/makeus/cmc/malmo/adaptor/out/AbstractOpenAiCompatibleApiClientTest.java b/src/test/java/makeus/cmc/malmo/adaptor/out/AbstractOpenAiCompatibleApiClientTest.java new file mode 100644 index 00000000..de52c3a5 --- /dev/null +++ b/src/test/java/makeus/cmc/malmo/adaptor/out/AbstractOpenAiCompatibleApiClientTest.java @@ -0,0 +1,85 @@ +package makeus.cmc.malmo.adaptor.out; + +import com.fasterxml.jackson.databind.ObjectMapper; +import makeus.cmc.malmo.application.port.out.chat.LlmReasoningScenario; +import makeus.cmc.malmo.config.properties.ReasoningEffortProperties; +import org.junit.jupiter.api.Test; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class AbstractOpenAiCompatibleApiClientTest { + + @Test + void createBody_usesScenarioSpecificReasoningEffort_whenPresent() { + TestClient client = new TestClient(reasoningEffortProperties("medium", Map.of( + LlmReasoningScenario.VALIDATION, "none" + ))); + + Map body = client.exposeCreateBody(LlmReasoningScenario.VALIDATION); + + assertThat(body.get("reasoning_effort")).isEqualTo("none"); + } + + @Test + void createBody_fallsBackToDefaultReasoningEffort_whenScenarioOverrideMissing() { + TestClient client = new TestClient(reasoningEffortProperties("medium", Map.of())); + + Map body = client.exposeCreateBody(LlmReasoningScenario.STRUCTURED_CHAT); + + assertThat(body.get("reasoning_effort")).isEqualTo("medium"); + } + + @Test + void createBody_omitsReasoningEffort_whenNoValuesConfigured() { + TestClient client = new TestClient(reasoningEffortProperties(null, Map.of())); + + Map body = client.exposeCreateBody(LlmReasoningScenario.SUMMARY); + + assertThat(body).doesNotContainKey("reasoning_effort"); + } + + private static ReasoningEffortProperties reasoningEffortProperties(String defaultEffort, + Map scenarios) { + ReasoningEffortProperties properties = new ReasoningEffortProperties(); + properties.setDefault(defaultEffort); + properties.setScenarios(scenarios); + return properties; + } + + private static final class TestClient extends AbstractOpenAiCompatibleApiClient { + private final ReasoningEffortProperties reasoningEffortProperties; + + private TestClient(ReasoningEffortProperties reasoningEffortProperties) { + super(WebClient.builder().baseUrl("https://example.com").build(), new ObjectMapper()); + this.reasoningEffortProperties = reasoningEffortProperties; + } + + private Map exposeCreateBody(LlmReasoningScenario scenario) { + return createBody(List.of(Map.of("role", "user", "content", "hello")), scenario, false, false); + } + + @Override + protected String getProviderName() { + return "Test"; + } + + @Override + protected String getApiKey() { + return "test-key"; + } + + @Override + protected String getModel() { + return "test-model"; + } + + @Override + protected ReasoningEffortProperties getReasoningEffortProperties() { + return reasoningEffortProperties; + } + } +} diff --git a/src/test/java/makeus/cmc/malmo/application/service/chat/ChatProcessorTest.java b/src/test/java/makeus/cmc/malmo/application/service/chat/ChatProcessorTest.java new file mode 100644 index 00000000..671fb5d2 --- /dev/null +++ b/src/test/java/makeus/cmc/malmo/application/service/chat/ChatProcessorTest.java @@ -0,0 +1,119 @@ +package makeus.cmc.malmo.application.service.chat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import makeus.cmc.malmo.application.port.in.chat.SufficiencyCheckResult; +import makeus.cmc.malmo.application.port.out.chat.LlmReasoningScenario; +import makeus.cmc.malmo.application.port.out.chat.RequestChatApiPort; +import makeus.cmc.malmo.domain.model.chat.DetailedPrompt; +import makeus.cmc.malmo.domain.model.chat.Prompt; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ChatProcessorTest { + + @Mock + private RequestChatApiPort requestChatApiPort; + + private ChatProcessor chatProcessor; + + @BeforeEach + void setUp() { + chatProcessor = new ChatProcessor(requestChatApiPort, new ObjectMapper()); + } + + @Test + void streamChat_usesProvidedScenario() { + when(requestChatApiPort.requestStreamResponse(anyList(), eq(LlmReasoningScenario.STRUCTURED_CHAT), any())) + .thenReturn(Mono.just("full answer")); + + List> messages = new ArrayList<>(); + chatProcessor.streamChat( + messages, + LlmReasoningScenario.STRUCTURED_CHAT, + Prompt.from(1L, 1, "system", false, false, false, false, false, false, false, null, null, null), + Prompt.from(2L, 1, "guideline", false, false, true, false, false, false, false, null, null, null), + DetailedPrompt.create(1, 1, "detail", false, false, null, false, true), + chunk -> { }, + full -> { }, + error -> { } + ) + .block(); + + verify(requestChatApiPort).requestStreamResponse(anyList(), eq(LlmReasoningScenario.STRUCTURED_CHAT), any()); + } + + @Test + void requestMetaData_usesAuxiliaryExtractionScenario() { + when(requestChatApiPort.requestResponse(anyList(), eq(LlmReasoningScenario.AUXILIARY_EXTRACTION))) + .thenReturn(CompletableFuture.completedFuture("memo")); + + String result = chatProcessor.requestMetaData("question", "answer", + Prompt.from(1L, 999, "metadata", false, false, false, false, false, true, false, null, null, null)) + .join(); + + assertThat(result).isEqualTo("memo"); + verify(requestChatApiPort).requestResponse(anyList(), eq(LlmReasoningScenario.AUXILIARY_EXTRACTION)); + } + + @Test + void requestSufficiencyCheck_usesValidationScenario() { + when(requestChatApiPort.requestJsonResponse(anyList(), eq(LlmReasoningScenario.VALIDATION))) + .thenReturn(CompletableFuture.completedFuture(""" + {"completed":true,"summary":"ok","advice":null} + """)); + + SufficiencyCheckResult result = chatProcessor.requestSufficiencyCheck( + new ArrayList<>(), + DetailedPrompt.create(1, 1, "validation", true, false, null, false, false) + ).join(); + + assertThat(result.isCompleted()).isTrue(); + assertThat(result.getSummary()).isEqualTo("ok"); + verify(requestChatApiPort).requestJsonResponse(anyList(), eq(LlmReasoningScenario.VALIDATION)); + } + + @Test + void requestConversationSummary_usesSummaryScenario() { + when(requestChatApiPort.requestResponse(anyList(), eq(LlmReasoningScenario.SUMMARY))) + .thenReturn(CompletableFuture.completedFuture("summary")); + + String result = chatProcessor.requestConversationSummary( + new ArrayList<>(), + Prompt.from(1L, 4, "summary prompt", false, true, false, false, false, false, false, null, null, null) + ).join(); + + assertThat(result).isEqualTo("summary"); + verify(requestChatApiPort).requestResponse(anyList(), eq(LlmReasoningScenario.SUMMARY)); + } + + @Test + void requestTitleGeneration_usesAuxiliaryExtractionScenario() { + when(requestChatApiPort.requestResponse(anyList(), eq(LlmReasoningScenario.AUXILIARY_EXTRACTION))) + .thenReturn(CompletableFuture.completedFuture("title")); + + String result = chatProcessor.requestTitleGeneration( + new ArrayList<>(), + Prompt.from(1L, 999, "title", false, false, false, false, false, false, true, null, null, null) + ).join(); + + assertThat(result).isEqualTo("title"); + verify(requestChatApiPort).requestResponse(anyList(), eq(LlmReasoningScenario.AUXILIARY_EXTRACTION)); + } +} diff --git a/src/test/java/makeus/cmc/malmo/config/LlmClientSelectionTest.java b/src/test/java/makeus/cmc/malmo/config/LlmClientSelectionTest.java new file mode 100644 index 00000000..e365d818 --- /dev/null +++ b/src/test/java/makeus/cmc/malmo/config/LlmClientSelectionTest.java @@ -0,0 +1,26 @@ +package makeus.cmc.malmo.config; + +import makeus.cmc.malmo.adaptor.out.OpenAiApiClient; +import makeus.cmc.malmo.application.port.out.CheckLlmHealth; +import makeus.cmc.malmo.application.port.out.chat.RequestChatApiPort; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = "spring.task.scheduling.enabled=false") +class LlmClientSelectionTest { + + @Autowired + private RequestChatApiPort requestChatApiPort; + + @Autowired + private CheckLlmHealth checkLlmHealth; + + @Test + void primaryLlmClient_isOpenAi() { + assertThat(requestChatApiPort).isInstanceOf(OpenAiApiClient.class); + assertThat(checkLlmHealth).isInstanceOf(OpenAiApiClient.class); + } +} diff --git a/src/test/java/makeus/cmc/malmo/config/LlmConfigurationPropertiesTest.java b/src/test/java/makeus/cmc/malmo/config/LlmConfigurationPropertiesTest.java new file mode 100644 index 00000000..0e606a87 --- /dev/null +++ b/src/test/java/makeus/cmc/malmo/config/LlmConfigurationPropertiesTest.java @@ -0,0 +1,48 @@ +package makeus.cmc.malmo.config; + +import makeus.cmc.malmo.application.port.out.chat.LlmReasoningScenario; +import makeus.cmc.malmo.config.properties.GeminiApiProperties; +import makeus.cmc.malmo.config.properties.OpenAiApiProperties; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = "spring.task.scheduling.enabled=false") +class LlmConfigurationPropertiesTest { + + @Autowired + private OpenAiApiProperties openAiApiProperties; + + @Autowired + private GeminiApiProperties geminiApiProperties; + + @Test + void openAiProperties_areBoundFromConfiguration() { + assertThat(openAiApiProperties.getKey()).isEqualTo("sk-test-openai-api-key-for-testing-only"); + assertThat(openAiApiProperties.getModel()).isEqualTo("gpt-5.4-mini"); + assertThat(openAiApiProperties.getBaseUrl()).isEqualTo("https://api.openai.com/v1"); + assertThat(openAiApiProperties.getStatusUrl()).isEqualTo("https://status.openai.com/api/v2/status.json"); + assertThat(openAiApiProperties.getReasoningEffort().getDefault()).isEqualTo("medium"); + assertThat(openAiApiProperties.getReasoningEffort().getScenarioEffort(LlmReasoningScenario.STRUCTURED_CHAT)).isEqualTo("low"); + assertThat(openAiApiProperties.getReasoningEffort().getScenarioEffort(LlmReasoningScenario.FREE_CONVERSATION)).isEqualTo("low"); + assertThat(openAiApiProperties.getReasoningEffort().getScenarioEffort(LlmReasoningScenario.VALIDATION)).isEqualTo("none"); + assertThat(openAiApiProperties.getReasoningEffort().getScenarioEffort(LlmReasoningScenario.SUMMARY)).isEqualTo("none"); + assertThat(openAiApiProperties.getReasoningEffort().getScenarioEffort(LlmReasoningScenario.AUXILIARY_EXTRACTION)).isEqualTo("none"); + } + + @Test + void geminiProperties_areBoundFromConfiguration() { + assertThat(geminiApiProperties.getKey()).isEqualTo("test-gemini-api-key-for-testing-only"); + assertThat(geminiApiProperties.getModel()).isEqualTo("gemini-3-flash-preview"); + assertThat(geminiApiProperties.getBaseUrl()) + .isEqualTo("https://generativelanguage.googleapis.com/v1beta/openai"); + assertThat(geminiApiProperties.getReasoningEffort().getDefault()).isEqualTo("high"); + assertThat(geminiApiProperties.getReasoningEffort().getScenarioEffort(LlmReasoningScenario.STRUCTURED_CHAT)).isEqualTo("medium"); + assertThat(geminiApiProperties.getReasoningEffort().getScenarioEffort(LlmReasoningScenario.FREE_CONVERSATION)).isEqualTo("low"); + assertThat(geminiApiProperties.getReasoningEffort().getScenarioEffort(LlmReasoningScenario.VALIDATION)).isEqualTo("low"); + assertThat(geminiApiProperties.getReasoningEffort().getScenarioEffort(LlmReasoningScenario.SUMMARY)).isEqualTo("low"); + assertThat(geminiApiProperties.getReasoningEffort().getScenarioEffort(LlmReasoningScenario.AUXILIARY_EXTRACTION)).isEqualTo("low"); + } +} From d1443a5cb7066884a5689300e3e40488dc194266 Mon Sep 17 00:00:00 2001 From: Choi wontak <76509639+RoundTable02@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:01:22 +0900 Subject: [PATCH 10/15] =?UTF-8?q?fix:=20=EC=95=A0=EC=B0=A9=EC=9C=A0?= =?UTF-8?q?=ED=98=95=20=EA=B8=B0=EB=B3=B8=20=EA=B0=92=EC=9D=84=20null?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/API-CHANGES-ONBOARDING-PROFILE.md | 26 +++- .../service/member/MemberCommandService.java | 6 +- .../MemberIntegrationTest.java | 144 ++++++++++++++++++ 3 files changed, 164 insertions(+), 12 deletions(-) diff --git a/docs/API-CHANGES-ONBOARDING-PROFILE.md b/docs/API-CHANGES-ONBOARDING-PROFILE.md index d1603300..ed713863 100644 --- a/docs/API-CHANGES-ONBOARDING-PROFILE.md +++ b/docs/API-CHANGES-ONBOARDING-PROFILE.md @@ -157,20 +157,32 @@ --- -### 4. PATCH /members/partners (상대 프로필 수정) +### 4. POST /members/partners (상대 프로필 최초 등록) -**변경 전:** `personalityType`, `loveTypeCategory` 모두 필수 +| 필드 | 타입 | 필수 여부 | 설명 | +|------|------|-----------|------| +| `personalityType` | String | **필수** | 상대방 MBTI (영문 4자리, 대소문자 무관) | +| `loveTypeCategory` | Enum | 선택 | 상대방 애착 유형 | -**변경 후:** 두 필드 모두 선택 사항 — 전달된 필드만 업데이트 (부분 업데이트 지원) +> **Note:** `loveTypeCategory`를 생략하면 `null`로 저장됩니다. `UNKNOWN`은 사용자가 명시적으로 선택한 경우에만 설정됩니다. + +**예시 - MBTI만 등록 (loveTypeCategory는 나중에 PATCH로 설정 가능):** +```json +{ "personalityType": "INTJ" } +``` -**Request:** +**예시 - 두 필드 모두 등록:** ```json { "personalityType": "INTJ", - "loveTypeCategory": "STABLE_TYPE" + "loveTypeCategory": "SECURE" } ``` +--- + +### 5. PATCH /members/partners (상대 프로필 수정) + | 필드 | 타입 | 필수 여부 | 설명 | |------|------|-----------|------| | `personalityType` | String | 선택 | 상대방 MBTI (영문 4자리, 대소문자 무관) | @@ -183,9 +195,9 @@ { "personalityType": "INTJ" } ``` -**예시 - loveTypeCategory만 수정:** +**예시 - loveTypeCategory만 수정 (MBTI만 등록한 후 애착유형 추가 시 사용):** ```json -{ "loveTypeCategory": "ANXIETY_TYPE" } +{ "loveTypeCategory": "SECURE" } ``` --- 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 6be7992b..20ba3694 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 @@ -84,8 +84,7 @@ public PartnerProfileResponseDto createPartnerProfile(CreatePartnerProfileComman throw new PartnerProfileAlreadyExistsException("이미 상대 프로필이 등록되어 있습니다."); } - PartnerLoveTypeCategory partnerLoveTypeCategory = resolvePartnerLoveTypeCategory(command.getLoveTypeCategory()); - member.createPartnerProfile(command.getPersonalityType(), partnerLoveTypeCategory); + member.createPartnerProfile(command.getPersonalityType(), command.getLoveTypeCategory()); Member savedMember = memberCommandHelper.saveMember(member); return toPartnerProfileResponse(savedMember); @@ -183,7 +182,4 @@ private PartnerProfileResponseDto toPartnerProfileResponse(Member savedMember) { .build(); } - private PartnerLoveTypeCategory resolvePartnerLoveTypeCategory(PartnerLoveTypeCategory loveTypeCategory) { - return loveTypeCategory == null ? PartnerLoveTypeCategory.UNKNOWN : loveTypeCategory; - } } 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 71ac057f..bc7ddb6b 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java @@ -1595,6 +1595,150 @@ private MemberEntity createAndSavePartner() { return partner; } + @Nested + @DisplayName("상대 프로필 등록/수정 API 테스트 (POST/PATCH /members/partners)") + class PartnerProfileTest { + + @Test + @DisplayName("MBTI만 전송하면 loveTypeCategory는 null로 저장된다") + void createPartnerProfile_mbtiOnly_loveTypeCategoryIsNull() throws Exception { + // given + Map requestDto = Map.of("personalityType", "INFP"); + + // when & then + mockMvc.perform(post("/members/partners") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.personalityType").value("INFP")) + .andExpect(jsonPath("$.data.loveTypeCategory").doesNotExist()); + + em.flush(); + em.clear(); + + MemberEntity savedMember = em.find(MemberEntity.class, member.getId()); + assertThat(savedMember.getOtherPersonalityType()).isEqualTo("INFP"); + assertThat(savedMember.getPartnerLoveTypeCategory()).isNull(); + } + + @Test + @DisplayName("MBTI와 loveTypeCategory 모두 전송하면 두 필드 모두 저장된다") + void createPartnerProfile_bothFields_allSaved() throws Exception { + // given + Map requestDto = Map.of( + "personalityType", "INFP", + "loveTypeCategory", "SECURE" + ); + + // when & then + mockMvc.perform(post("/members/partners") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.personalityType").value("INFP")) + .andExpect(jsonPath("$.data.loveTypeCategory").value("SECURE")); + + em.flush(); + em.clear(); + + MemberEntity savedMember = em.find(MemberEntity.class, member.getId()); + assertThat(savedMember.getOtherPersonalityType()).isEqualTo("INFP"); + assertThat(savedMember.getPartnerLoveTypeCategory()).isEqualTo(PartnerLoveTypeCategory.SECURE); + } + + @Test + @DisplayName("UNKNOWN을 명시적으로 전송하면 UNKNOWN으로 저장된다") + void createPartnerProfile_unknownExplicit_savedAsUnknown() throws Exception { + // given + Map requestDto = Map.of( + "personalityType", "INFP", + "loveTypeCategory", "UNKNOWN" + ); + + // when & then + mockMvc.perform(post("/members/partners") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.loveTypeCategory").value("UNKNOWN")); + + em.flush(); + em.clear(); + + MemberEntity savedMember = em.find(MemberEntity.class, member.getId()); + assertThat(savedMember.getPartnerLoveTypeCategory()).isEqualTo(PartnerLoveTypeCategory.UNKNOWN); + } + + @Test + @DisplayName("MBTI 없이 loveTypeCategory만 전송하면 400 오류가 발생한다") + void createPartnerProfile_missingMbti_returns400() throws Exception { + // given + Map requestDto = Map.of("loveTypeCategory", "SECURE"); + + // when & then + mockMvc.perform(post("/members/partners") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("MBTI만 등록한 후 PATCH로 loveTypeCategory를 설정할 수 있다") + void updatePartnerProfile_addLoveTypeAfterMbtiOnly() throws Exception { + // given - MBTI만 먼저 등록 + mockMvc.perform(post("/members/partners") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(Map.of("personalityType", "INFP")))) + .andExpect(status().isOk()); + + // when - loveTypeCategory만 PATCH + mockMvc.perform(patch("/members/partners") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(Map.of("loveTypeCategory", "SECURE")))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.personalityType").value("INFP")) + .andExpect(jsonPath("$.data.loveTypeCategory").value("SECURE")); + + em.flush(); + em.clear(); + + MemberEntity savedMember = em.find(MemberEntity.class, member.getId()); + assertThat(savedMember.getOtherPersonalityType()).isEqualTo("INFP"); + assertThat(savedMember.getPartnerLoveTypeCategory()).isEqualTo(PartnerLoveTypeCategory.SECURE); + } + + @Test + @DisplayName("PATCH에서 loveTypeCategory를 생략하면 기존 값이 유지된다") + void updatePartnerProfile_omitLoveType_preservesExistingValue() throws Exception { + // given - MBTI + loveTypeCategory 모두 등록 + mockMvc.perform(post("/members/partners") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(Map.of( + "personalityType", "INFP", + "loveTypeCategory", "SECURE" + )))) + .andExpect(status().isOk()); + + // when - personalityType만 PATCH (loveTypeCategory 생략) + mockMvc.perform(patch("/members/partners") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(Map.of("personalityType", "INTJ")))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.personalityType").value("INTJ")) + .andExpect(jsonPath("$.data.loveTypeCategory").value("SECURE")); + } + } + private CoupleQuestionEntity createAndSaveCoupleQuestion(QuestionEntity questionEntity, Integer coupleId) { CoupleQuestionEntity coupleQuestion = CoupleQuestionEntity.builder() .question(questionEntity) From b0c0c8083d14113e5685f4ee66b03dac2629ec04 Mon Sep 17 00:00:00 2001 From: Choi wontak <76509639+RoundTable02@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:20:19 +0900 Subject: [PATCH 11/15] =?UTF-8?q?test:=20=ED=83=80=EC=9E=85=20=EB=B6=88?= =?UTF-8?q?=EC=9D=BC=EC=B9=98=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=8B=A4=ED=8C=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/API-CHANGES-ONBOARDING-PROFILE.md | 8 ++++---- .../MemberIntegrationTest.java | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/API-CHANGES-ONBOARDING-PROFILE.md b/docs/API-CHANGES-ONBOARDING-PROFILE.md index ed713863..50143d09 100644 --- a/docs/API-CHANGES-ONBOARDING-PROFILE.md +++ b/docs/API-CHANGES-ONBOARDING-PROFILE.md @@ -69,7 +69,7 @@ "nickname": "닉네임", "email": "user@example.com", "provider": "KAKAO", - "loveTypeCategory": "SECURE", + "loveTypeCategory": "STABLE_TYPE", "anxietyRate": 0.3, "avoidanceRate": 0.2, "inviteCode": "ABC123", @@ -86,7 +86,7 @@ "nickname": "닉네임", "email": "user@example.com", "provider": "KAKAO", - "loveTypeCategory": "SECURE", + "loveTypeCategory": "STABLE_TYPE", "anxietyRate": 0.3, "avoidanceRate": 0.2, "inviteCode": "ABC123", @@ -175,7 +175,7 @@ ```json { "personalityType": "INTJ", - "loveTypeCategory": "SECURE" + "loveTypeCategory": "STABLE_TYPE" } ``` @@ -197,7 +197,7 @@ **예시 - loveTypeCategory만 수정 (MBTI만 등록한 후 애착유형 추가 시 사용):** ```json -{ "loveTypeCategory": "SECURE" } +{ "loveTypeCategory": "STABLE_TYPE" } ``` --- 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 bc7ddb6b..16c92d70 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java @@ -1629,7 +1629,7 @@ void createPartnerProfile_bothFields_allSaved() throws Exception { // given Map requestDto = Map.of( "personalityType", "INFP", - "loveTypeCategory", "SECURE" + "loveTypeCategory", "STABLE_TYPE" ); // when & then @@ -1640,14 +1640,14 @@ void createPartnerProfile_bothFields_allSaved() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data.personalityType").value("INFP")) - .andExpect(jsonPath("$.data.loveTypeCategory").value("SECURE")); + .andExpect(jsonPath("$.data.loveTypeCategory").value("STABLE_TYPE")); em.flush(); em.clear(); MemberEntity savedMember = em.find(MemberEntity.class, member.getId()); assertThat(savedMember.getOtherPersonalityType()).isEqualTo("INFP"); - assertThat(savedMember.getPartnerLoveTypeCategory()).isEqualTo(PartnerLoveTypeCategory.SECURE); + assertThat(savedMember.getPartnerLoveTypeCategory()).isEqualTo(PartnerLoveTypeCategory.STABLE_TYPE); } @Test @@ -1678,7 +1678,7 @@ void createPartnerProfile_unknownExplicit_savedAsUnknown() throws Exception { @DisplayName("MBTI 없이 loveTypeCategory만 전송하면 400 오류가 발생한다") void createPartnerProfile_missingMbti_returns400() throws Exception { // given - Map requestDto = Map.of("loveTypeCategory", "SECURE"); + Map requestDto = Map.of("loveTypeCategory", "STABLE_TYPE"); // when & then mockMvc.perform(post("/members/partners") @@ -1702,17 +1702,17 @@ void updatePartnerProfile_addLoveTypeAfterMbtiOnly() throws Exception { mockMvc.perform(patch("/members/partners") .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(Map.of("loveTypeCategory", "SECURE")))) + .content(objectMapper.writeValueAsString(Map.of("loveTypeCategory", "STABLE_TYPE")))) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.personalityType").value("INFP")) - .andExpect(jsonPath("$.data.loveTypeCategory").value("SECURE")); + .andExpect(jsonPath("$.data.loveTypeCategory").value("STABLE_TYPE")); em.flush(); em.clear(); MemberEntity savedMember = em.find(MemberEntity.class, member.getId()); assertThat(savedMember.getOtherPersonalityType()).isEqualTo("INFP"); - assertThat(savedMember.getPartnerLoveTypeCategory()).isEqualTo(PartnerLoveTypeCategory.SECURE); + assertThat(savedMember.getPartnerLoveTypeCategory()).isEqualTo(PartnerLoveTypeCategory.STABLE_TYPE); } @Test @@ -1724,7 +1724,7 @@ void updatePartnerProfile_omitLoveType_preservesExistingValue() throws Exception .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(Map.of( "personalityType", "INFP", - "loveTypeCategory", "SECURE" + "loveTypeCategory", "STABLE_TYPE" )))) .andExpect(status().isOk()); @@ -1735,7 +1735,7 @@ void updatePartnerProfile_omitLoveType_preservesExistingValue() throws Exception .content(objectMapper.writeValueAsString(Map.of("personalityType", "INTJ")))) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.personalityType").value("INTJ")) - .andExpect(jsonPath("$.data.loveTypeCategory").value("SECURE")); + .andExpect(jsonPath("$.data.loveTypeCategory").value("STABLE_TYPE")); } } From 628165a9b164e041be458a288827f3c3b8abafcb Mon Sep 17 00:00:00 2001 From: Choi wontak <76509639+RoundTable02@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:29:42 +0900 Subject: [PATCH 12/15] =?UTF-8?q?test:=20=EC=95=A0=EC=B0=A9=EC=9C=A0?= =?UTF-8?q?=ED=98=95=20=EB=AF=B8=EA=B2=B0=EC=A0=95=20=EC=83=81=ED=99=A9=20?= =?UTF-8?q?null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../malmo/integration_test/MemberIntegrationTest.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 16c92d70..84f0d5d4 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java @@ -1035,6 +1035,7 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc .content(objectMapper.writeValueAsString(Map.of("personalityType", "enfp")))) .andExpect(status().isOk()); + // loveTypeCategory를 null로 전달하면 기존 값(null) 유지 mockMvc.perform(patch("/members/partners") .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) @@ -1043,8 +1044,8 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.personalityType").value("INTJ")) - .andExpect(jsonPath("$.data.loveTypeCategory").value("UNKNOWN")) - .andExpect(jsonPath("$.data.description").value("모르겠어요")); + .andExpect(jsonPath("$.data.loveTypeCategory").doesNotExist()) + .andExpect(jsonPath("$.data.description").doesNotExist()); // when MvcResult mvcResult = mockMvc.perform(get("/members/partner") @@ -1062,8 +1063,8 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc PartnerResponseDto partnerDto = responseDto.data; Assertions.assertThat(partnerDto.personalityType).isEqualTo("INTJ"); - Assertions.assertThat(partnerDto.loveTypeCategory).isEqualTo("UNKNOWN"); - Assertions.assertThat(partnerDto.description).isEqualTo("모르겠어요"); + Assertions.assertThat(partnerDto.loveTypeCategory).isNull(); + Assertions.assertThat(partnerDto.description).isNull(); } @Test From cb8b5b75559396d855dee35601c526bfbc2c311c Mon Sep 17 00:00:00 2001 From: Choi wontak <76509639+RoundTable02@users.noreply.github.com> Date: Wed, 25 Mar 2026 01:01:50 +0900 Subject: [PATCH 13/15] =?UTF-8?q?feat:=202=EB=8B=A8=EA=B3=84=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20=EC=A7=81=ED=9B=84=20=EC=83=81=EB=8C=80=20=EC=95=A0?= =?UTF-8?q?=EC=B0=A9=EC=9C=A0=ED=98=95=20=EC=B6=94=EB=A1=A0=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/chat/ChatMessageService.java | 70 +++- .../service/chat/ChatProcessor.java | 28 ++ .../service/chat/ChatPromptBuilder.java | 21 ++ .../cmc/malmo/domain/model/member/Member.java | 11 + .../cmc/malmo/util/GlobalConstants.java | 18 + .../service/chat/ChatMessageServiceTest.java | 352 ++++++++++++++++++ .../service/chat/ChatProcessorTest.java | 44 +++ 7 files changed, 534 insertions(+), 10 deletions(-) create mode 100644 src/test/java/makeus/cmc/malmo/application/service/chat/ChatMessageServiceTest.java 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 380be34d..47abf61b 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 @@ -8,6 +8,7 @@ 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.MemberChatRoomMetadataCommandHelper; +import makeus.cmc.malmo.application.helper.member.MemberCommandHelper; import makeus.cmc.malmo.application.helper.member.MemberMemoryCommandHelper; import makeus.cmc.malmo.application.helper.member.MemberQueryHelper; import makeus.cmc.malmo.application.helper.question.CoupleQuestionQueryHelper; @@ -30,7 +31,9 @@ import makeus.cmc.malmo.domain.value.id.CoupleId; import makeus.cmc.malmo.domain.value.id.CoupleQuestionId; import makeus.cmc.malmo.domain.value.id.MemberId; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; import makeus.cmc.malmo.util.ChatMessageSplitter; +import makeus.cmc.malmo.util.GlobalConstants; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -60,6 +63,7 @@ public class ChatMessageService implements ProcessMessageUseCase { private final CoupleQuestionQueryHelper coupleQuestionQueryHelper; + private final MemberCommandHelper memberCommandHelper; private final MemberMemoryCommandHelper memberMemoryCommandHelper; private final makeus.cmc.malmo.application.helper.outbox.OutboxHelper outboxHelper; @@ -105,16 +109,7 @@ public CompletableFuture processStreamChatMessage(ProcessMessageCommand co } // 마지막 충분성 조건인 경우 - // 1단계 종료 시 제목 생성 요청 - if (command.getPromptLevel() == 1) { - requestTitleGenerationAsync(chatRoom); - } - - // 다음 단계 오프닝 생성 성공 후 단계 전이 - return requestNextStageOpening(member, chatRoom, command) - .thenRun(() -> - chatRoomCommandHelper.upgradeChatRoomLevel(chatRoom.getId(), command.getPromptLevel() + 1, 1) - ); + return handleCompletedStage(member, chatRoom, command); }); } @@ -281,6 +276,61 @@ private CompletableFuture requestNextStageOpening(Member member, ChatRoom ).toFuture(); } + private CompletableFuture handleCompletedStage( + Member member, + ChatRoom chatRoom, + ProcessMessageCommand command + ) { + if (command.getPromptLevel() == 1) { + requestTitleGenerationAsync(chatRoom); + } + + return requestNextStageOpening(member, chatRoom, command) + .thenCompose(ignored -> inferAndPersistPartnerLoveTypeIfNeeded(member, chatRoom, command)) + .thenRun(() -> + chatRoomCommandHelper.upgradeChatRoomLevel(chatRoom.getId(), command.getPromptLevel() + 1, 1) + ); + } + + private CompletableFuture inferAndPersistPartnerLoveTypeIfNeeded( + Member member, + ChatRoom chatRoom, + ProcessMessageCommand command + ) { + if (!shouldInferPartnerLoveType(member, command)) { + return CompletableFuture.completedFuture(null); + } + + List> messages = chatPromptBuilder.createForPartnerLoveTypeInference( + member, chatRoom, command.getPromptLevel() + 1); + + return chatProcessor.requestPartnerLoveTypeCategoryInference( + messages, + GlobalConstants.PARTNER_LOVE_TYPE_INFERENCE_PROMPT + ) + .thenAccept(inferredType -> persistPartnerLoveTypeIfStillUnknown(command.getMemberId(), inferredType)) + .exceptionally(throwable -> { + log.warn("Failed to infer partner love type for memberId={}, chatRoomId={}", + command.getMemberId(), chatRoom.getId(), throwable); + return null; + }); + } + + private boolean shouldInferPartnerLoveType(Member member, ProcessMessageCommand command) { + return command.getPromptLevel() == 1 + && (member.getPartnerLoveTypeCategory() == null + || member.getPartnerLoveTypeCategory() == PartnerLoveTypeCategory.UNKNOWN); + } + + private void persistPartnerLoveTypeIfStillUnknown(Long memberId, PartnerLoveTypeCategory inferredType) { + Member refreshedMember = memberQueryHelper.getMemberByIdOrThrow(MemberId.of(memberId)); + boolean updated = refreshedMember.updatePartnerLoveTypeCategoryIfUnknown(inferredType); + if (!updated) { + return; + } + memberCommandHelper.saveMember(refreshedMember); + } + /** * 비동기 제목 생성 요청 * Redis Stream을 통해 제목 생성 워커에 전달 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 71e2417b..1ba3e2c4 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 @@ -1,6 +1,7 @@ package makeus.cmc.malmo.application.service.chat; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -9,6 +10,7 @@ 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.PartnerLoveTypeCategory; import makeus.cmc.malmo.domain.value.type.SenderType; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; @@ -78,6 +80,32 @@ public CompletableFuture requestMetaData(String question, return requestChatApiPort.requestResponse(messages, LlmReasoningScenario.AUXILIARY_EXTRACTION); } + public CompletableFuture requestPartnerLoveTypeCategoryInference( + List> messages, + String inferencePrompt + ) { + messages.add(createMessageMap(SenderType.SYSTEM, inferencePrompt)); + + return requestChatApiPort.requestJsonResponse(messages, LlmReasoningScenario.AUXILIARY_EXTRACTION) + .thenApply(jsonResponse -> { + try { + JsonNode node = objectMapper.readTree(jsonResponse); + String rawValue = node.path("partnerLoveTypeCategory").asText(null); + if (rawValue == null || rawValue.isBlank()) { + throw new IllegalArgumentException("partnerLoveTypeCategory is required"); + } + PartnerLoveTypeCategory partnerLoveTypeCategory = PartnerLoveTypeCategory.valueOf(rawValue); + if (partnerLoveTypeCategory == PartnerLoveTypeCategory.UNKNOWN) { + throw new IllegalArgumentException("UNKNOWN is not allowed for inferred partner love type"); + } + return partnerLoveTypeCategory; + } catch (JsonProcessingException e) { + log.error("Failed to parse partner love type inference JSON: {}", jsonResponse, e); + throw new RuntimeException("Failed to parse partner love type inference JSON", e); + } + }); + } + public CompletableFuture requestSufficiencyCheck(List> messages, DetailedPrompt validationPrompt) { messages.add(createMessageMap(SenderType.SYSTEM, validationPrompt.getContent())); 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 fdb46d94..e571cb24 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 @@ -234,6 +234,27 @@ public List> createForNextStage(Member member, ChatRoom chat return messages; } + public List> createForPartnerLoveTypeInference(Member member, ChatRoom chatRoom, int targetLevel) { + List> messages = new ArrayList<>(); + ChatRoomId chatRoomId = ChatRoomId.of(chatRoom.getId()); + + String metaDataContent = getMetaDataContent(member); + messages.add(createMessageMap(SenderType.SYSTEM, metaDataContent)); + + List metadataList = memberChatRoomMetadataQueryHelper.getMemberChatRoomMetadata(chatRoomId); + if (!metadataList.isEmpty()) { + String metadataContent = getMemberChatRoomMetadataContent(metadataList); + messages.add(createMessageMap(SenderType.SYSTEM, metadataContent)); + } + + List stageMessages = chatRoomQueryHelper.getChatRoomLevelMessages(chatRoomId, targetLevel); + for (ChatMessage chatMessage : stageMessages) { + messages.add(createMessageMap(chatMessage.getSenderType(), chatMessage.getContent())); + } + + return messages; + } + private String getMemberChatRoomMetadataContent(List metadataList) { if (metadataList == null || metadataList.isEmpty()) { return ""; 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 50bcb7f8..ea4a7292 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 @@ -176,6 +176,17 @@ public void updatePartnerProfile(String otherPersonalityType, PartnerLoveTypeCat } } + public boolean updatePartnerLoveTypeCategoryIfUnknown(PartnerLoveTypeCategory partnerLoveTypeCategory) { + if (partnerLoveTypeCategory == null) { + return false; + } + if (this.partnerLoveTypeCategory != null && this.partnerLoveTypeCategory != PartnerLoveTypeCategory.UNKNOWN) { + return false; + } + this.partnerLoveTypeCategory = partnerLoveTypeCategory; + return true; + } + public void updateLoveType(LoveTypeCategory loveTypeCategory, float avoidanceRate, float anxietyRate) { this.loveTypeCategory = loveTypeCategory; this.avoidanceRate = avoidanceRate; diff --git a/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java b/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java index e0cc110d..6583afce 100644 --- a/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java +++ b/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java @@ -18,6 +18,24 @@ public class GlobalConstants { public static final String ATTACHMENT_TYPE_PROMPT_MESSAGE = "잠깐! 애착유형 테스트를 하면, 더 정확한 상담이 가능해! 그대로 진행하면 바로 상담해줄게"; + + public static final String PARTNER_LOVE_TYPE_INFERENCE_PROMPT = """ + 너는 연애 상담 대화의 2단계 분석 내용을 바탕으로 상대방의 애착유형을 추론하는 분류기야. + 지금 주어지는 컨텍스트는 2단계 상담 맥락, 2단계 대화 내용, 그리고 챗봇이 수행한 2단계 분석을 포함한다. + 반드시 이 2단계 분석과 동일한 맥락만 사용해서 상대방의 애착유형을 추론해라. + + [분류 규칙] + - partnerLoveTypeCategory는 반드시 다음 4개 중 하나만 선택한다. + STABLE_TYPE + ANXIETY_TYPE + AVOIDANCE_TYPE + CONFUSION_TYPE + - UNKNOWN은 절대 반환하지 않는다. + - 설명, 이유, 추가 텍스트 없이 JSON 객체만 반환한다. + + [응답 형식] + {"partnerLoveTypeCategory":"STABLE_TYPE"} + """; // 커플 복구 관련 상수 public static final int COUPLE_RECOVERY_LIMIT_DAYS = 30; diff --git a/src/test/java/makeus/cmc/malmo/application/service/chat/ChatMessageServiceTest.java b/src/test/java/makeus/cmc/malmo/application/service/chat/ChatMessageServiceTest.java new file mode 100644 index 00000000..c2e0c627 --- /dev/null +++ b/src/test/java/makeus/cmc/malmo/application/service/chat/ChatMessageServiceTest.java @@ -0,0 +1,352 @@ +package makeus.cmc.malmo.application.service.chat; + +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.DetailedPromptQueryHelper; +import makeus.cmc.malmo.application.helper.chat_room.MemberChatRoomMetadataCommandHelper; +import makeus.cmc.malmo.application.helper.chat_room.PromptQueryHelper; +import makeus.cmc.malmo.application.helper.member.MemberCommandHelper; +import makeus.cmc.malmo.application.helper.member.MemberMemoryCommandHelper; +import makeus.cmc.malmo.application.helper.member.MemberQueryHelper; +import makeus.cmc.malmo.application.helper.outbox.OutboxHelper; +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.domain.model.chat.ChatRoom; +import makeus.cmc.malmo.domain.model.chat.DetailedPrompt; +import makeus.cmc.malmo.domain.model.chat.MemberChatRoomMetadata; +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.InviteCodeValue; +import makeus.cmc.malmo.domain.value.id.MemberId; +import makeus.cmc.malmo.domain.value.state.ChatRoomState; +import makeus.cmc.malmo.domain.value.state.MemberState; +import makeus.cmc.malmo.domain.value.type.EmailForwardingStatus; +import makeus.cmc.malmo.domain.value.type.MemberRole; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; +import makeus.cmc.malmo.domain.value.type.Provider; +import makeus.cmc.malmo.domain.value.type.RelationshipStatus; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ChatMessageService 테스트") +class ChatMessageServiceTest { + + @Mock + private MemberQueryHelper memberQueryHelper; + + @Mock + private ChatRoomQueryHelper chatRoomQueryHelper; + + @Mock + private PromptQueryHelper promptQueryHelper; + + @Mock + private DetailedPromptQueryHelper detailedPromptQueryHelper; + + @Mock + private MemberChatRoomMetadataCommandHelper memberChatRoomMetadataCommandHelper; + + @Mock + private ChatPromptBuilder chatPromptBuilder; + + @Mock + private ChatProcessor chatProcessor; + + @Mock + private ChatSseSender chatSseSender; + + @Mock + private ChatRoomCommandHelper chatRoomCommandHelper; + + @Mock + private ChatRoomDomainService chatRoomDomainService; + + @Mock + private CoupleQuestionQueryHelper coupleQuestionQueryHelper; + + @Mock + private MemberMemoryCommandHelper memberMemoryCommandHelper; + + @Mock + private OutboxHelper outboxHelper; + + @Mock + private MemberCommandHelper memberCommandHelper; + + @InjectMocks + private ChatMessageService chatMessageService; + + @Test + @DisplayName("1단계 완료 후 2단계 첫 분석 메시지 생성 직후 partnerLoveTypeCategory를 추론해 저장한다") + void processStreamChatMessage_infersAndPersistsPartnerLoveTypeAfterStage2Opening() { + Member stage2Member = createMember(1L, null); + Member refreshedMember = createMember(1L, null); + Member savedMember = createMember(1L, PartnerLoveTypeCategory.CONFUSION_TYPE); + ChatRoom chatRoom = createChatRoom(10L, 1, 2); + DetailedPrompt lastDetailedPrompt = DetailedPrompt.create(1, 2, "guideline", false, false, "상황 요약", true, true); + Prompt systemPrompt = Prompt.from(1L, 0, "system", true, false, false, false, false, false, false, null, null, null); + Prompt nextPrompt = Prompt.from(2L, 2, "next", false, false, false, false, true, false, false, null, null, null); + DetailedPrompt nextDetailedPrompt = DetailedPrompt.create(2, 1, "next-detailed", false, false, "분석", true, true); + ProcessMessageUseCase.ProcessMessageCommand command = ProcessMessageUseCase.ProcessMessageCommand.builder() + .memberId(1L) + .chatRoomId(10L) + .nowMessage("메시지") + .promptLevel(1) + .detailedLevel(2) + .build(); + SufficiencyCheckResult result = SufficiencyCheckResult.builder() + .completed(true) + .summary("1단계 상황 요약") + .build(); + + stubCompletedFlow(stage2Member, refreshedMember, chatRoom, lastDetailedPrompt, systemPrompt, nextPrompt, nextDetailedPrompt, result, savedMember); + + when(chatProcessor.requestPartnerLoveTypeCategoryInference(anyList(), anyString())) + .thenReturn(CompletableFuture.completedFuture(PartnerLoveTypeCategory.CONFUSION_TYPE)); + + chatMessageService.processStreamChatMessage(command).join(); + + ArgumentCaptor savedMemberCaptor = ArgumentCaptor.forClass(Member.class); + verify(memberCommandHelper).saveMember(savedMemberCaptor.capture()); + assertThat(savedMemberCaptor.getValue().getPartnerLoveTypeCategory()).isEqualTo(PartnerLoveTypeCategory.CONFUSION_TYPE); + + verify(chatPromptBuilder).createForPartnerLoveTypeInference(stage2Member, chatRoom, 2); + verify(chatPromptBuilder).createForNextStage(stage2Member, chatRoom, 2); + verify(chatRoomCommandHelper).upgradeChatRoomLevel(10L, 2, 1); + } + + @Test + @DisplayName("이미 partnerLoveTypeCategory가 확정되어 있으면 추론과 저장을 생략한다") + void processStreamChatMessage_skipsInferenceWhenPartnerLoveTypeAlreadyKnown() { + Member stage2Member = createMember(1L, PartnerLoveTypeCategory.ANXIETY_TYPE); + ChatRoom chatRoom = createChatRoom(10L, 1, 2); + DetailedPrompt lastDetailedPrompt = DetailedPrompt.create(1, 2, "guideline", false, false, "상황 요약", true, true); + Prompt systemPrompt = Prompt.from(1L, 0, "system", true, false, false, false, false, false, false, null, null, null); + Prompt nextPrompt = Prompt.from(2L, 2, "next", false, false, false, false, true, false, false, null, null, null); + DetailedPrompt nextDetailedPrompt = DetailedPrompt.create(2, 1, "next-detailed", false, false, "분석", true, true); + ProcessMessageUseCase.ProcessMessageCommand command = ProcessMessageUseCase.ProcessMessageCommand.builder() + .memberId(1L) + .chatRoomId(10L) + .nowMessage("메시지") + .promptLevel(1) + .detailedLevel(2) + .build(); + SufficiencyCheckResult result = SufficiencyCheckResult.builder() + .completed(true) + .summary("1단계 상황 요약") + .build(); + + when(memberQueryHelper.getMemberByIdOrThrow(MemberId.of(1L))).thenReturn(stage2Member); + when(chatRoomQueryHelper.getChatRoomByIdOrThrow(any())).thenReturn(chatRoom); + when(chatProcessor.requestSufficiencyCheck(anyList(), any())).thenReturn(CompletableFuture.completedFuture(result)); + when(chatPromptBuilder.createForSufficiencyCheck(stage2Member, chatRoom, 1, 2)).thenReturn(List.of()); + when(detailedPromptQueryHelper.getValidationPrompt(1, 2)).thenReturn(java.util.Optional.of(lastDetailedPrompt)); + when(detailedPromptQueryHelper.getGuidelinePrompt(1, 2)).thenReturn(java.util.Optional.of(lastDetailedPrompt)); + when(promptQueryHelper.getSystemPrompt()).thenReturn(systemPrompt); + when(promptQueryHelper.getGuidelinePromptWithFallback(2)).thenReturn(nextPrompt); + when(detailedPromptQueryHelper.getGuidelinePromptWithFallback(2, 1)).thenReturn(nextDetailedPrompt); + when(chatPromptBuilder.createForNextStage(stage2Member, chatRoom, 2)).thenReturn(List.of()); + stubStreamChatCompletion(); + + chatMessageService.processStreamChatMessage(command).join(); + + verify(chatProcessor, never()).requestPartnerLoveTypeCategoryInference(anyList(), anyString()); + verify(memberCommandHelper, never()).saveMember(any()); + verify(chatPromptBuilder).createForNextStage(stage2Member, chatRoom, 2); + } + + @Test + @DisplayName("2단계 첫 분석 메시지 이후 추론이 실패해도 저장하지 않고 흐름은 계속 진행한다") + void processStreamChatMessage_continuesWhenInferenceFailsAfterStage2Opening() { + Member stage2Member = createMember(1L, PartnerLoveTypeCategory.UNKNOWN); + Member refreshedMember = createMember(1L, PartnerLoveTypeCategory.UNKNOWN); + ChatRoom chatRoom = createChatRoom(10L, 1, 2); + DetailedPrompt lastDetailedPrompt = DetailedPrompt.create(1, 2, "guideline", false, false, "상황 요약", true, true); + Prompt systemPrompt = Prompt.from(1L, 0, "system", true, false, false, false, false, false, false, null, null, null); + Prompt nextPrompt = Prompt.from(2L, 2, "next", false, false, false, false, true, false, false, null, null, null); + DetailedPrompt nextDetailedPrompt = DetailedPrompt.create(2, 1, "next-detailed", false, false, "분석", true, true); + ProcessMessageUseCase.ProcessMessageCommand command = ProcessMessageUseCase.ProcessMessageCommand.builder() + .memberId(1L) + .chatRoomId(10L) + .nowMessage("메시지") + .promptLevel(1) + .detailedLevel(2) + .build(); + SufficiencyCheckResult result = SufficiencyCheckResult.builder() + .completed(true) + .summary("1단계 상황 요약") + .build(); + + when(memberQueryHelper.getMemberByIdOrThrow(MemberId.of(1L))).thenReturn(stage2Member, refreshedMember); + when(chatRoomQueryHelper.getChatRoomByIdOrThrow(any())).thenReturn(chatRoom); + when(chatProcessor.requestSufficiencyCheck(anyList(), any())).thenReturn(CompletableFuture.completedFuture(result)); + when(chatPromptBuilder.createForSufficiencyCheck(stage2Member, chatRoom, 1, 2)).thenReturn(List.of()); + when(detailedPromptQueryHelper.getValidationPrompt(1, 2)).thenReturn(java.util.Optional.of(lastDetailedPrompt)); + when(detailedPromptQueryHelper.getGuidelinePrompt(1, 2)).thenReturn(java.util.Optional.of(lastDetailedPrompt)); + when(chatPromptBuilder.createForPartnerLoveTypeInference(stage2Member, chatRoom, 2)) + .thenReturn(List.of(Map.of("role", "system", "content", "context"))); + when(chatProcessor.requestPartnerLoveTypeCategoryInference(anyList(), anyString())) + .thenReturn(CompletableFuture.failedFuture(new IllegalArgumentException("invalid"))); + when(promptQueryHelper.getSystemPrompt()).thenReturn(systemPrompt); + when(promptQueryHelper.getGuidelinePromptWithFallback(2)).thenReturn(nextPrompt); + when(detailedPromptQueryHelper.getGuidelinePromptWithFallback(2, 1)).thenReturn(nextDetailedPrompt); + when(chatPromptBuilder.createForNextStage(stage2Member, chatRoom, 2)).thenReturn(List.of()); + stubStreamChatCompletion(); + + chatMessageService.processStreamChatMessage(command).join(); + + verify(memberCommandHelper, never()).saveMember(any()); + verify(chatPromptBuilder).createForNextStage(stage2Member, chatRoom, 2); + verify(chatRoomCommandHelper).upgradeChatRoomLevel(10L, 2, 1); + } + + @Test + @DisplayName("2단계 종료 시점에는 더 이상 추론하지 않는다") + void processStreamChatMessage_skipsInferenceAtEndOfStage2() { + Member stage2Member = createMember(1L, null); + ChatRoom chatRoom = createChatRoom(10L, 2, 2); + DetailedPrompt lastDetailedPrompt = DetailedPrompt.create(2, 2, "guideline", false, false, "분석", true, true); + ProcessMessageUseCase.ProcessMessageCommand command = ProcessMessageUseCase.ProcessMessageCommand.builder() + .memberId(1L) + .chatRoomId(10L) + .nowMessage("메시지") + .promptLevel(2) + .detailedLevel(2) + .build(); + SufficiencyCheckResult result = SufficiencyCheckResult.builder() + .completed(true) + .summary("2단계 분석") + .build(); + + when(memberQueryHelper.getMemberByIdOrThrow(MemberId.of(1L))).thenReturn(stage2Member); + when(chatRoomQueryHelper.getChatRoomByIdOrThrow(any())).thenReturn(chatRoom); + when(chatProcessor.requestSufficiencyCheck(anyList(), any())).thenReturn(CompletableFuture.completedFuture(result)); + when(chatPromptBuilder.createForSufficiencyCheck(stage2Member, chatRoom, 2, 2)).thenReturn(List.of()); + when(detailedPromptQueryHelper.getValidationPrompt(2, 2)).thenReturn(java.util.Optional.of(lastDetailedPrompt)); + when(detailedPromptQueryHelper.getGuidelinePrompt(2, 2)).thenReturn(java.util.Optional.of(lastDetailedPrompt)); + when(promptQueryHelper.getSystemPrompt()).thenReturn(Prompt.from(1L, 0, "system", true, false, false, false, false, false, false, null, null, null)); + when(promptQueryHelper.getGuidelinePromptWithFallback(3)).thenReturn(Prompt.from(2L, 3, "stage3", false, false, false, false, true, false, false, null, null, null)); + when(detailedPromptQueryHelper.getGuidelinePromptWithFallback(3, 1)).thenReturn(DetailedPrompt.create(3, 1, "next", false, false, "다음", true, true)); + when(chatPromptBuilder.createForNextStage(stage2Member, chatRoom, 3)).thenReturn(List.of()); + stubStreamChatCompletion(); + + chatMessageService.processStreamChatMessage(command).join(); + + verify(chatProcessor, never()).requestPartnerLoveTypeCategoryInference(anyList(), anyString()); + verify(chatRoomCommandHelper).upgradeChatRoomLevel(10L, 3, 1); + } + + private void stubCompletedFlow( + Member stage2Member, + Member refreshedMember, + ChatRoom chatRoom, + DetailedPrompt lastDetailedPrompt, + Prompt systemPrompt, + Prompt nextPrompt, + DetailedPrompt nextDetailedPrompt, + SufficiencyCheckResult result, + Member nextStageMember + ) { + when(memberQueryHelper.getMemberByIdOrThrow(MemberId.of(1L))) + .thenReturn(stage2Member, refreshedMember); + when(chatRoomQueryHelper.getChatRoomByIdOrThrow(any())).thenReturn(chatRoom); + when(chatProcessor.requestSufficiencyCheck(anyList(), any())).thenReturn(CompletableFuture.completedFuture(result)); + when(chatPromptBuilder.createForSufficiencyCheck(stage2Member, chatRoom, 1, 2)).thenReturn(List.of()); + when(detailedPromptQueryHelper.getValidationPrompt(1, 2)).thenReturn(java.util.Optional.of(lastDetailedPrompt)); + when(detailedPromptQueryHelper.getGuidelinePrompt(1, 2)).thenReturn(java.util.Optional.of(lastDetailedPrompt)); + when(chatPromptBuilder.createForPartnerLoveTypeInference(stage2Member, chatRoom, 2)) + .thenReturn(List.of(Map.of("role", "system", "content", "context"))); + when(memberCommandHelper.saveMember(any())).thenReturn(nextStageMember); + when(promptQueryHelper.getSystemPrompt()).thenReturn(systemPrompt); + when(promptQueryHelper.getGuidelinePromptWithFallback(2)).thenReturn(nextPrompt); + when(detailedPromptQueryHelper.getGuidelinePromptWithFallback(2, 1)).thenReturn(nextDetailedPrompt); + when(chatPromptBuilder.createForNextStage(stage2Member, chatRoom, 2)).thenReturn(List.of()); + stubStreamChatCompletion(); + } + + private void stubStreamChatCompletion() { + doAnswer(invocation -> { + Consumer onComplete = invocation.getArgument(6); + onComplete.accept("다음 단계 응답"); + return Mono.empty(); + }).when(chatProcessor).streamChat(anyList(), any(), any(), any(), any(), any(), any(), any()); + } + + private Member createMember(Long id, PartnerLoveTypeCategory partnerLoveTypeCategory) { + LocalDateTime now = LocalDateTime.now(); + return Member.from( + id, + Provider.KAKAO, + "provider-id", + MemberRole.MEMBER, + MemberState.ALIVE, + false, + null, + null, + null, + 0.0f, + 0.0f, + "tak", + "tak@example.com", + EmailForwardingStatus.ENABLED, + InviteCodeValue.of("ABCD1234"), + null, + null, + null, + RelationshipStatus.IN_RELATIONSHIP, + "INTJ", + "ENFP", + partnerLoveTypeCategory, + now, + now, + null + ); + } + + private ChatRoom createChatRoom(Long id, int level, int detailedLevel) { + LocalDateTime now = LocalDateTime.now(); + return ChatRoom.from( + id, + MemberId.of(1L), + ChatRoomState.ALIVE, + level, + detailedLevel, + now, + null, + null, + null, + null, + null, + null, + now, + now, + null + ); + } +} diff --git a/src/test/java/makeus/cmc/malmo/application/service/chat/ChatProcessorTest.java b/src/test/java/makeus/cmc/malmo/application/service/chat/ChatProcessorTest.java index 671fb5d2..62b5c670 100644 --- a/src/test/java/makeus/cmc/malmo/application/service/chat/ChatProcessorTest.java +++ b/src/test/java/makeus/cmc/malmo/application/service/chat/ChatProcessorTest.java @@ -6,6 +6,7 @@ import makeus.cmc.malmo.application.port.out.chat.RequestChatApiPort; import makeus.cmc.malmo.domain.model.chat.DetailedPrompt; import makeus.cmc.malmo.domain.model.chat.Prompt; +import makeus.cmc.malmo.domain.value.type.PartnerLoveTypeCategory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -19,6 +20,7 @@ import java.util.concurrent.CompletableFuture; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; @@ -116,4 +118,46 @@ void requestTitleGeneration_usesAuxiliaryExtractionScenario() { assertThat(result).isEqualTo("title"); verify(requestChatApiPort).requestResponse(anyList(), eq(LlmReasoningScenario.AUXILIARY_EXTRACTION)); } + + @Test + void requestPartnerLoveTypeCategoryInference_usesAuxiliaryExtractionScenario() { + when(requestChatApiPort.requestJsonResponse(anyList(), eq(LlmReasoningScenario.AUXILIARY_EXTRACTION))) + .thenReturn(CompletableFuture.completedFuture(""" + {"partnerLoveTypeCategory":"AVOIDANCE_TYPE"} + """)); + + PartnerLoveTypeCategory result = chatProcessor.requestPartnerLoveTypeCategoryInference( + new ArrayList<>(), + "prompt" + ).join(); + + assertThat(result).isEqualTo(PartnerLoveTypeCategory.AVOIDANCE_TYPE); + verify(requestChatApiPort).requestJsonResponse(anyList(), eq(LlmReasoningScenario.AUXILIARY_EXTRACTION)); + } + + @Test + void requestPartnerLoveTypeCategoryInference_rejectsUnknown() { + when(requestChatApiPort.requestJsonResponse(anyList(), eq(LlmReasoningScenario.AUXILIARY_EXTRACTION))) + .thenReturn(CompletableFuture.completedFuture(""" + {"partnerLoveTypeCategory":"UNKNOWN"} + """)); + + assertThatThrownBy(() -> chatProcessor.requestPartnerLoveTypeCategoryInference( + new ArrayList<>(), + "prompt" + ).join()).hasRootCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + void requestPartnerLoveTypeCategoryInference_rejectsInvalidValue() { + when(requestChatApiPort.requestJsonResponse(anyList(), eq(LlmReasoningScenario.AUXILIARY_EXTRACTION))) + .thenReturn(CompletableFuture.completedFuture(""" + {"partnerLoveTypeCategory":"NOT_A_TYPE"} + """)); + + assertThatThrownBy(() -> chatProcessor.requestPartnerLoveTypeCategoryInference( + new ArrayList<>(), + "prompt" + ).join()).hasRootCauseInstanceOf(IllegalArgumentException.class); + } } From e3e45ef0ca50dcb109c36a0621736e17b88c3d3b Mon Sep 17 00:00:00 2001 From: Choi wontak <76509639+RoundTable02@users.noreply.github.com> Date: Wed, 25 Mar 2026 01:05:34 +0900 Subject: [PATCH 14/15] =?UTF-8?q?docs:=202=EB=8B=A8=EA=B3=84=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20=ED=9B=84=20=EC=83=81=EB=8C=80=20=EC=95=A0=EC=B0=A9?= =?UTF-8?q?=EC=9C=A0=ED=98=95=20=EC=B6=94=EB=A1=A0=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=AC=B8=EC=84=9C=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...S-CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE.md | 4 + docs/API-CHANGES-LOVETYPE-DATA.md | 5 +- docs/API-CHANGES-ONBOARDING-PROFILE.md | 2 +- ...-PARTNER-LOVETYPE-INFERENCE-PERSISTENCE.md | 105 ++++++++++++++++++ 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 docs/API-CHANGES-PARTNER-LOVETYPE-INFERENCE-PERSISTENCE.md diff --git a/docs/API-CHANGES-CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE.md b/docs/API-CHANGES-CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE.md index 6409df1a..98b98b86 100644 --- a/docs/API-CHANGES-CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE.md +++ b/docs/API-CHANGES-CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE.md @@ -73,6 +73,7 @@ UNKNOWN, 사용자와의 대화로부터 유추할 것 - `otherPersonalityType`가 있는 경우에만 `- 상대방 성향 프롬프트:` 항목을 추가합니다. - `partnerLoveTypeCategory`가 `UNKNOWN` 또는 `null`이면 DB 조회 없이 바로 폴백 문구를 삽입합니다. +- 다만 1단계 완료 후 생성되는 **2단계 첫 분석 메시지 직후** 내부 추론이 성공하면, 이후부터는 저장된 확정값으로 조회됩니다. - `otherPersonalityType`와 확정된 `partnerLoveTypeCategory`가 모두 있으면 `(otherPersonalityType, partnerLoveTypeCategory)` 조합으로 조회합니다. - 조합 row가 없으면 채팅은 실패시키지 않고 동일한 폴백 문구를 삽입합니다. @@ -115,6 +116,9 @@ UNKNOWN, 사용자와의 대화로부터 유추할 것 - `GET /members`의 `otherPersonalityType` - `GET /members`의 `partnerLoveTypeCategory` +추가로, 사용자가 상대방 애착유형을 입력하지 않았더라도 1단계 완료 후 2단계 첫 분석 메시지 직후 내부 추론 결과가 `partnerLoveTypeCategory`에 저장될 수 있습니다. +관련 상세 내용은 `docs/API-CHANGES-PARTNER-LOVETYPE-INFERENCE-PERSISTENCE.md`를 참고합니다. + --- ## 테스트 diff --git a/docs/API-CHANGES-LOVETYPE-DATA.md b/docs/API-CHANGES-LOVETYPE-DATA.md index 3ae405f4..9a8a1977 100644 --- a/docs/API-CHANGES-LOVETYPE-DATA.md +++ b/docs/API-CHANGES-LOVETYPE-DATA.md @@ -54,6 +54,7 @@ otherPersonalityType: string, // 상대 MBTI partnerLoveTypeCategory: 'STABLE_TYPE' | 'ANXIETY_TYPE' | 'AVOIDANCE_TYPE' | 'CONFUSION_TYPE' | 'UNKNOWN' // undefined = 미입력 / UNKNOWN = "모르겠어요" 선택됨 + // 또는 1단계 완료 후 2단계 첫 분석 메시지 직후 내부 추론값이 저장될 수 있음 } ``` @@ -71,6 +72,7 @@ - 상대방 - `otherPersonalityType`가 있을 때만 상대방 성향 프롬프트 항목이 추가됩니다. - `partnerLoveTypeCategory`가 `UNKNOWN` 또는 `null`이면 DB 조회 없이 `UNKNOWN, 사용자와의 대화로부터 유추할 것`을 사용합니다. + - 이후 1단계 완료 후 생성되는 2단계 첫 분석 메시지 직후 내부 추론이 성공하면, 저장된 확정값으로 프롬프트를 조회합니다. - `otherPersonalityType`와 확정된 `partnerLoveTypeCategory`가 모두 있으면 `(otherPersonalityType, partnerLoveTypeCategory)` 조합으로 상세 프롬프트를 조회합니다. - 매칭 row가 없으면 채팅은 실패하지 않고 동일한 폴백 문구를 사용합니다. @@ -81,4 +83,5 @@ - `lovetype`: `STABLE_TYPE | ANXIETY_TYPE | AVOIDANCE_TYPE | CONFUSION_TYPE` - `prompts`: 실제 채팅 메타데이터에 삽입할 프롬프트 전문 -상세 동작 예시는 `docs/API-CHANGES-CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE.md`, 실제 전달 예시는 `docs/CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE-EXAMPLE.md`를 참고합니다. +상세 동작 예시는 `docs/API-CHANGES-CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE.md`, 실제 전달 예시는 `docs/CHAT-PROMPT-PERSONALITY-TYPE-LOVETYPE-EXAMPLE.md`, +영속화 동작은 `docs/API-CHANGES-PARTNER-LOVETYPE-INFERENCE-PERSISTENCE.md`를 참고합니다. diff --git a/docs/API-CHANGES-ONBOARDING-PROFILE.md b/docs/API-CHANGES-ONBOARDING-PROFILE.md index 50143d09..24da61ff 100644 --- a/docs/API-CHANGES-ONBOARDING-PROFILE.md +++ b/docs/API-CHANGES-ONBOARDING-PROFILE.md @@ -164,7 +164,7 @@ | `personalityType` | String | **필수** | 상대방 MBTI (영문 4자리, 대소문자 무관) | | `loveTypeCategory` | Enum | 선택 | 상대방 애착 유형 | -> **Note:** `loveTypeCategory`를 생략하면 `null`로 저장됩니다. `UNKNOWN`은 사용자가 명시적으로 선택한 경우에만 설정됩니다. +> **Note:** `loveTypeCategory`를 생략하면 `null`로 저장됩니다. `UNKNOWN`은 사용자가 명시적으로 선택한 경우에 설정됩니다. 다만 이후 채팅에서 1단계 완료 후 2단계 첫 분석 메시지 직후 내부 추론을 통해 확정값으로 갱신될 수 있습니다. **예시 - MBTI만 등록 (loveTypeCategory는 나중에 PATCH로 설정 가능):** ```json diff --git a/docs/API-CHANGES-PARTNER-LOVETYPE-INFERENCE-PERSISTENCE.md b/docs/API-CHANGES-PARTNER-LOVETYPE-INFERENCE-PERSISTENCE.md new file mode 100644 index 00000000..d2cb91fc --- /dev/null +++ b/docs/API-CHANGES-PARTNER-LOVETYPE-INFERENCE-PERSISTENCE.md @@ -0,0 +1,105 @@ +# 상대방 애착유형 내부 추론 영속화 변경 사항 + +## 개요 + +사용자가 상대방 애착유형(`partnerLoveTypeCategory`)을 직접 입력하지 않았거나 `UNKNOWN`으로 남겨둔 경우, +1단계 상황 수집이 완료된 뒤 생성되는 **2단계 첫 분석 메시지 직후**에 챗봇이 상대방 애착유형을 내부적으로 추론하고 `member.partnerLoveTypeCategory`에 저장합니다. + +이번 변경의 목적은 다음과 같습니다. + +1. 1단계에서 수집된 갈등 상황 정보를 바탕으로 더 이른 시점에 상대방 애착유형을 확정하기 +2. 2단계 분석 메시지와 동일한 맥락을 사용해 추론 결과의 일관성을 높이기 +3. 3단계 이후 프롬프트에서 확정된 상대방 애착유형을 재사용해 상담 품질을 높이기 + +--- + +## 동작 시점 + +추론은 다음 조건을 모두 만족할 때 실행됩니다. + +- 현재 완료된 단계가 `1단계`의 마지막 세부 단계임 +- 1단계 완료 후 시스템이 `2단계`의 첫 분석 메시지를 생성함 +- `partnerLoveTypeCategory`가 `null` 또는 `UNKNOWN`임 + +즉, **2단계 종료 시점이 아니라 2단계 첫 분석 메시지가 생성된 직후**에 실행됩니다. + +--- + +## 추론 규칙 + +추론은 별도 내부 프롬프트(`GlobalConstants.PARTNER_LOVE_TYPE_INFERENCE_PROMPT`)를 사용하며, +입력 컨텍스트에는 아래 내용이 포함됩니다. + +- 사용자 메타데이터 +- 이전 단계 요약 메타데이터 +- 2단계 레벨의 대화 메시지들 + - 여기에는 방금 생성된 챗봇의 2단계 첫 분석 메시지가 포함됩니다. + +반환값은 아래 4개 enum 중 하나만 허용됩니다. + +- `STABLE_TYPE` +- `ANXIETY_TYPE` +- `AVOIDANCE_TYPE` +- `CONFUSION_TYPE` + +`UNKNOWN`은 추론 결과로 허용하지 않습니다. + +응답 형식은 JSON 객체로 고정됩니다. + +```json +{ + "partnerLoveTypeCategory": "STABLE_TYPE" +} +``` + +--- + +## 저장 규칙 + +- 저장 대상 필드: `member.partnerLoveTypeCategory` +- 저장 조건: + - 현재 DB 값이 `null` 또는 `UNKNOWN`일 때만 반영 + - 이미 사용자가 직접 입력한 확정값이 있으면 덮어쓰지 않음 +- 저장 직전에는 `Member`를 다시 조회해 최신 DB 상태를 확인합니다. + +### 실패 처리 + +아래 경우에는 저장하지 않고 상담 흐름만 계속 진행합니다. + +- JSON 파싱 실패 +- enum 값 검증 실패 +- `UNKNOWN` 반환 +- 저장 직전 재조회 결과 이미 확정값이 존재함 +- 영속화 과정 예외 발생 + +--- + +## 외부 API 영향 + +외부 요청/응답 스펙 변경은 없습니다. + +다만 `GET /members`의 `partnerLoveTypeCategory`는 다음과 같은 값이 들어올 수 있습니다. + +- 사용자가 직접 입력한 값 +- 사용자가 `UNKNOWN`으로 설정한 값 +- 내부 2단계 분석 직후 추론되어 저장된 값 + +즉, 클라이언트는 `partnerLoveTypeCategory`가 이전보다 더 이른 시점에 확정될 수 있음을 고려해야 합니다. + +--- + +## 테스트 + +검증한 항목: + +- `AUXILIARY_EXTRACTION` 시나리오로 JSON 추론 호출 +- `UNKNOWN` 및 비정상 enum 값 거부 +- 1단계 완료 후 2단계 첫 분석 메시지 직후 추론 및 저장 +- 이미 확정된 상대방 애착유형이 있으면 추론/저장 생략 +- 추론 실패 시 저장 없이 다음 단계 진행 +- 2단계 종료 시점에는 더 이상 추론하지 않음 + +관련 테스트: + +- `src/test/java/makeus/cmc/malmo/application/service/chat/ChatProcessorTest.java` +- `src/test/java/makeus/cmc/malmo/application/service/chat/ChatMessageServiceTest.java` From 4555207051b8d6753d02411e34f0ccb6f32092f2 Mon Sep 17 00:00:00 2001 From: Choi wontak <76509639+RoundTable02@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:43:45 +0900 Subject: [PATCH 15/15] =?UTF-8?q?docs:=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=EC=9A=A9=20=ED=86=B5=ED=95=A9=20SQL?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqls/MM-180.sql | 7 --- sqls/MM-181.sql | 15 ------ sqls/MM-188-release-migration.sql | 82 +++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 22 deletions(-) delete mode 100644 sqls/MM-180.sql delete mode 100644 sqls/MM-181.sql create mode 100644 sqls/MM-188-release-migration.sql diff --git a/sqls/MM-180.sql b/sqls/MM-180.sql deleted file mode 100644 index 54975a5d..00000000 --- a/sqls/MM-180.sql +++ /dev/null @@ -1,7 +0,0 @@ --- MM-180: Add direct partner love type field to member_entity --- Author: Codex --- Date: 2026-03-16 - -ALTER TABLE member_entity -ADD COLUMN partner_love_type_category VARCHAR(255); -COMMENT ON COLUMN member_entity.partner_love_type_category IS 'Partner love type category: STABLE_TYPE, ANXIETY_TYPE, AVOIDANCE_TYPE, CONFUSION_TYPE, UNKNOWN'; diff --git a/sqls/MM-181.sql b/sqls/MM-181.sql deleted file mode 100644 index 092745d0..00000000 --- a/sqls/MM-181.sql +++ /dev/null @@ -1,15 +0,0 @@ --- MM-181: Add love_type_personality_type_prompt table for chat prompt enrichment --- Author: Codex --- Date: 2026-03-16 - -CREATE TABLE love_type_personality_type_prompt ( - personality_type VARCHAR(4) NOT NULL, - lovetype VARCHAR(255) NOT NULL, - prompts TEXT, - PRIMARY KEY (personality_type, lovetype) -); - -COMMENT ON TABLE love_type_personality_type_prompt IS 'Personality type and love type specific prompt snippets for chat metadata'; -COMMENT ON COLUMN love_type_personality_type_prompt.personality_type IS 'MBTI in 4-letter uppercase format'; -COMMENT ON COLUMN love_type_personality_type_prompt.lovetype IS 'Love type category: STABLE_TYPE, ANXIETY_TYPE, AVOIDANCE_TYPE, CONFUSION_TYPE'; -COMMENT ON COLUMN love_type_personality_type_prompt.prompts IS 'Prompt content to inject into chat metadata'; diff --git a/sqls/MM-188-release-migration.sql b/sqls/MM-188-release-migration.sql new file mode 100644 index 00000000..be20bf02 --- /dev/null +++ b/sqls/MM-188-release-migration.sql @@ -0,0 +1,82 @@ +-- MM-188 release migration +-- Target DB: MySQL 8.x +-- +-- Applies the schema required by: +-- 1. Direct partner love type persistence on member_entity +-- 2. MBTI + love type prompt enrichment +-- 3. MBTI + love type detailed result lookup +-- +-- NOTE: +-- - Run this once before deploying the application version that contains MM-188. +-- - This file intentionally replaces the split MM-180/MM-181 migration contents for release deployment. +-- - Production uses spring.jpa.hibernate.ddl-auto=none, so both tables below must exist before traffic reaches +-- /love-types/result or chat prompt enrichment paths. + +ALTER TABLE member_entity + ADD COLUMN partner_love_type_category VARCHAR(255) NULL + COMMENT 'Partner love type category: STABLE_TYPE, ANXIETY_TYPE, AVOIDANCE_TYPE, CONFUSION_TYPE, UNKNOWN'; + +CREATE TABLE love_type_personality_type_prompt ( + personality_type VARCHAR(4) NOT NULL COMMENT 'MBTI in 4-letter uppercase format', + lovetype VARCHAR(255) NOT NULL COMMENT 'Love type category: STABLE_TYPE, ANXIETY_TYPE, AVOIDANCE_TYPE, CONFUSION_TYPE', + prompts TEXT NULL COMMENT 'Prompt content to inject into chat metadata', + PRIMARY KEY (personality_type, lovetype) +) COMMENT='Personality type and love type specific prompt snippets for chat metadata'; + +CREATE TABLE love_type_personality_type_feature ( + personality_type VARCHAR(4) NOT NULL COMMENT 'MBTI in 4-letter uppercase format', + lovetype VARCHAR(255) NOT NULL COMMENT 'Love type category: STABLE_TYPE, ANXIETY_TYPE, AVOIDANCE_TYPE, CONFUSION_TYPE', + summary TEXT NULL, + keyword1 VARCHAR(255) NULL, + keyword2 VARCHAR(255) NULL, + keyword3 VARCHAR(255) NULL, + strength1 VARCHAR(255) NULL, + strength2 VARCHAR(255) NULL, + strength3 VARCHAR(255) NULL, + weakness VARCHAR(255) NULL, + strength_desc1 TEXT NULL, + strength_desc2 TEXT NULL, + strength_desc3 TEXT NULL, + weakness_desc TEXT NULL, + pattern_title1 VARCHAR(255) NULL, + pattern_title2 VARCHAR(255) NULL, + pattern_title3 VARCHAR(255) NULL, + pattern_title4 VARCHAR(255) NULL, + pattern1 TEXT NULL, + pattern2 TEXT NULL, + pattern3 TEXT NULL, + pattern4 TEXT NULL, + lovetype_feature_title1 VARCHAR(255) NULL, + lovetype_feature_title2 VARCHAR(255) NULL, + lovetype_feature_title3 VARCHAR(255) NULL, + lovetype_feature_title4 VARCHAR(255) NULL, + lovetype_feature1 TEXT NULL, + lovetype_feature2 TEXT NULL, + lovetype_feature3 TEXT NULL, + lovetype_feature4 TEXT NULL, + dating_guide1 TEXT NULL, + dating_guide2 TEXT NULL, + dating_guide3 TEXT NULL, + best_personality_type1 VARCHAR(4) NULL, + best_desc1 TEXT NULL, + best_personality_type2 VARCHAR(4) NULL, + best_desc2 TEXT NULL, + worst_personality_type1 VARCHAR(4) NULL, + worst_desc1 TEXT NULL, + worst_personality_type2 VARCHAR(4) NULL, + worst_desc2 TEXT NULL, + PRIMARY KEY (personality_type, lovetype) +) COMMENT='Detailed result content for MBTI and love type combinations'; + +-- Required production seed data: +-- Insert rows for all supported personality_type/lovetype combinations before enabling the related features. +-- The repository currently contains only test sample content, so real production copy/data should be loaded from +-- the product-approved source of truth. +-- +-- Minimum expected coverage: +-- - love_type_personality_type_prompt: rows used by ChatPromptBuilder for user/partner prompt enrichment. +-- - love_type_personality_type_feature: rows served by GET /love-types/result. +-- +-- Verification queries: +-- SELECT COUNT(*) AS prompt_count FROM love_type_personality_type_prompt; +-- SELECT COUNT(*) AS feature_count FROM love_type_personality_type_feature;