From 834b43ccd8f60a73acd8bbd2b16a3f28a8991598 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Tue, 28 Apr 2026 21:27:22 +0900 Subject: [PATCH 1/6] =?UTF-8?q?test:=20=EC=9C=A0=EC=A0=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/service/UserProfileServiceTest.java | 274 ++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 src/test/java/com/techfork/domain/user/service/UserProfileServiceTest.java diff --git a/src/test/java/com/techfork/domain/user/service/UserProfileServiceTest.java b/src/test/java/com/techfork/domain/user/service/UserProfileServiceTest.java new file mode 100644 index 00000000..d3eadf65 --- /dev/null +++ b/src/test/java/com/techfork/domain/user/service/UserProfileServiceTest.java @@ -0,0 +1,274 @@ +package com.techfork.domain.user.service; + +import com.techfork.domain.activity.entity.Bookmark; +import com.techfork.domain.activity.entity.ReadPost; +import com.techfork.domain.activity.entity.SearchHistory; +import com.techfork.domain.activity.repository.BookmarkRepository; +import com.techfork.domain.activity.repository.ReadPostRepository; +import com.techfork.domain.activity.repository.SearchHistoryRepository; +import com.techfork.domain.post.entity.Post; +import com.techfork.domain.post.entity.PostKeyword; +import com.techfork.domain.recommendation.service.RecommendationService; +import com.techfork.domain.user.document.UserProfileDocument; +import com.techfork.domain.user.entity.User; +import com.techfork.domain.user.entity.UserInterestCategory; +import com.techfork.domain.user.entity.UserInterestKeyword; +import com.techfork.domain.user.enums.EInterestCategory; +import com.techfork.domain.user.enums.EInterestKeyword; +import com.techfork.domain.user.enums.SocialType; +import com.techfork.domain.user.repository.UserInterestCategoryRepository; +import com.techfork.domain.user.repository.UserProfileDocumentRepository; +import com.techfork.domain.user.repository.UserRepository; +import com.techfork.global.llm.EmbeddingClient; +import com.techfork.global.llm.LlmClient; +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 org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class UserProfileServiceTest { + + @Mock + private UserInterestCategoryRepository userInterestCategoryRepository; + + @Mock + private ReadPostRepository readPostRepository; + + @Mock + private BookmarkRepository bookmarkRepository; + + @Mock + private SearchHistoryRepository searchHistoryRepository; + + @Mock + private UserProfileDocumentRepository userProfileDocumentRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private RecommendationService recommendationService; + + @Mock + private LlmClient llmClient; + + @Mock + private EmbeddingClient embeddingClient; + + @InjectMocks + private UserProfileService userProfileService; + + @Test + @DisplayName("사용자 활동 데이터를 모아 개인화 프로필을 생성하고 저장한다") + void generateUserProfileSync_CollectsActivityDataParsesAndSavesProfile() { + Long userId = 1L; + User user = createUser(userId); + List readPosts = List.of( + readPost("30초 포스트", List.of("Java"), 20), + readPost("90초 포스트", List.of("Spring"), 60), + readPost("300초 포스트", List.of("JPA"), 200), + readPost("600초 포스트", List.of("Kafka"), 400), + readPost("601초 포스트", List.of("Docker"), 700), + readPost("null 포스트", List.of("Elastic"), null) + ); + List bookmarks = List.of( + bookmark("북마크 포스트", List.of("Kubernetes", "Helm")) + ); + + given(userInterestCategoryRepository.findByUserIdWithKeywords(userId)) + .willReturn(List.of( + interestCategory(user, EInterestCategory.BACKEND, EInterestKeyword.JAVA, EInterestKeyword.SPRING), + interestCategory(user, EInterestCategory.DEVOPS, EInterestKeyword.DOCKER) + )); + given(readPostRepository.findRecentReadPostsByUserIdWithMinDuration(anyLong(), any())) + .willReturn(readPosts); + given(bookmarkRepository.findRecentBookmarksByUserId(anyLong(), any())) + .willReturn(bookmarks); + given(searchHistoryRepository.findRecentSearchHistoriesByUserId(anyLong(), any())) + .willReturn(List.of( + SearchHistory.create(user, "Spring Batch", LocalDateTime.now()), + SearchHistory.create(user, "Elasticsearch vector", LocalDateTime.now()) + )); + given(llmClient.call(anyString(), anyString())) + .willReturn(""" + ### PROFILE + Java와 Spring 기반 백엔드, Docker 중심 운영 자동화, Elasticsearch 검색 최적화에 집중하는 사용자 + + ### KEYWORDS + Java, Spring, Docker, Elasticsearch, Batch + """); + given(embeddingClient.embed(anyString())).willReturn(List.of(0.1f, 0.2f, 0.3f)); + given(userProfileDocumentRepository.save(any(UserProfileDocument.class))) + .willAnswer(invocation -> invocation.getArgument(0)); + given(userRepository.findById(userId)).willReturn(Optional.of(user)); + given(recommendationService.generateRecommendationsForUser(user)).willReturn(5); + + userProfileService.generateUserProfileSync(userId); + + ArgumentCaptor promptCaptor = ArgumentCaptor.forClass(String.class); + verify(llmClient).call(anyString(), promptCaptor.capture()); + + String prompt = promptCaptor.getValue(); + assertThat(prompt) + .contains("Java") + .contains("Spring") + .contains("Docker") + .contains("30초 포스트") + .contains("90초 포스트") + .contains("300초 포스트") + .contains("600초 포스트") + .contains("601초 포스트") + .contains("null 포스트") + .contains("북마크 포스트") + .contains("Spring Batch") + .contains("Elasticsearch vector") + .contains("가볍게 훑어봄") + .contains("빠르게 읽음") + .contains("읽음") + .contains("정독함") + .contains("깊게 읽음"); + + ArgumentCaptor documentCaptor = ArgumentCaptor.forClass(UserProfileDocument.class); + verify(userProfileDocumentRepository).save(documentCaptor.capture()); + + UserProfileDocument savedDocument = documentCaptor.getValue(); + assertThat(savedDocument.getUserId()).isEqualTo(userId); + assertThat(savedDocument.getProfileText()) + .isEqualTo("Java와 Spring 기반 백엔드, Docker 중심 운영 자동화, Elasticsearch 검색 최적화에 집중하는 사용자"); + assertThat(savedDocument.getProfileVector()).containsExactly(0.1f, 0.2f, 0.3f); + assertThat(savedDocument.getInterests()).containsExactly("Java", "Spring", "Docker"); + assertThat(savedDocument.getKeyKeywords()) + .containsExactly("Java", "Spring", "Docker", "Elasticsearch", "Batch"); + + verify(userRepository).findById(userId); + verify(recommendationService).generateRecommendationsForUser(user); + } + + @Test + @DisplayName("LLM 응답을 파싱하지 못하면 전체 텍스트를 프로필로 fallback 저장한다") + void generateUserProfileSync_FallsBackToFullTextWhenSectionsAreMissing() { + Long userId = 2L; + User user = createUser(userId); + String llmResponse = "섹션 없이도 전체 응답을 개인화 프로필로 저장해야 한다"; + + given(userInterestCategoryRepository.findByUserIdWithKeywords(userId)).willReturn(List.of()); + given(readPostRepository.findRecentReadPostsByUserIdWithMinDuration(anyLong(), any())).willReturn(List.of()); + given(bookmarkRepository.findRecentBookmarksByUserId(anyLong(), any())).willReturn(List.of()); + given(searchHistoryRepository.findRecentSearchHistoriesByUserId(anyLong(), any())).willReturn(List.of()); + given(llmClient.call(anyString(), anyString())).willReturn(llmResponse); + given(embeddingClient.embed(llmResponse)).willReturn(List.of(1.0f, 2.0f)); + given(userProfileDocumentRepository.save(any(UserProfileDocument.class))) + .willAnswer(invocation -> invocation.getArgument(0)); + given(userRepository.findById(userId)).willReturn(Optional.of(user)); + + userProfileService.generateUserProfileSync(userId); + + ArgumentCaptor documentCaptor = ArgumentCaptor.forClass(UserProfileDocument.class); + verify(userProfileDocumentRepository).save(documentCaptor.capture()); + + UserProfileDocument savedDocument = documentCaptor.getValue(); + assertThat(savedDocument.getProfileText()).isEqualTo(llmResponse); + assertThat(savedDocument.getKeyKeywords()).isEmpty(); + assertThat(savedDocument.getProfileVector()).containsExactly(1.0f, 2.0f); + } + + @Test + @DisplayName("추천 생성이 실패해도 개인화 프로필 저장은 유지된다") + void generateUserProfileSync_RecommendationFailureDoesNotBreakProfileSave() { + Long userId = 3L; + User user = createUser(userId); + + given(userInterestCategoryRepository.findByUserIdWithKeywords(userId)).willReturn(List.of()); + given(readPostRepository.findRecentReadPostsByUserIdWithMinDuration(anyLong(), any())).willReturn(List.of()); + given(bookmarkRepository.findRecentBookmarksByUserId(anyLong(), any())).willReturn(List.of()); + given(searchHistoryRepository.findRecentSearchHistoriesByUserId(anyLong(), any())).willReturn(List.of()); + given(llmClient.call(anyString(), anyString())) + .willReturn(""" + ### PROFILE + 추천 실패와 무관하게 저장되어야 하는 프로필 + + ### KEYWORDS + 테스트, 회귀 + """); + given(embeddingClient.embed(anyString())).willReturn(List.of(9.0f, 8.0f)); + given(userProfileDocumentRepository.save(any(UserProfileDocument.class))) + .willAnswer(invocation -> invocation.getArgument(0)); + given(userRepository.findById(userId)).willReturn(Optional.of(user)); + given(recommendationService.generateRecommendationsForUser(user)) + .willThrow(new RuntimeException("recommendation failure")); + + assertThatCode(() -> userProfileService.generateUserProfileSync(userId)) + .doesNotThrowAnyException(); + + verify(userProfileDocumentRepository).save(any(UserProfileDocument.class)); + verify(recommendationService).generateRecommendationsForUser(user); + } + + private User createUser(Long userId) { + User user = User.createSocialUser(SocialType.KAKAO, "social-" + userId, "user" + userId + "@example.com", null); + ReflectionTestUtils.setField(user, "id", userId); + return user; + } + + private UserInterestCategory interestCategory(User user, EInterestCategory category, EInterestKeyword... keywords) { + UserInterestCategory userInterestCategory = UserInterestCategory.create(user, category); + for (EInterestKeyword keyword : keywords) { + userInterestCategory.addKeyword(UserInterestKeyword.create(userInterestCategory, keyword)); + } + return userInterestCategory; + } + + private ReadPost readPost(String title, List keywords, Integer durationSeconds) { + ReadPost readPost = org.mockito.Mockito.mock(ReadPost.class); + Post post = mockPost(title, keywords); + + given(readPost.getPost()).willReturn(post); + given(readPost.getReadDurationSeconds()).willReturn(durationSeconds); + + return readPost; + } + + private Bookmark bookmark(String title, List keywords) { + Bookmark bookmark = org.mockito.Mockito.mock(Bookmark.class); + Post post = mockPost(title, keywords); + + given(bookmark.getPost()).willReturn(post); + + return bookmark; + } + + private Post mockPost(String title, List keywords) { + Post post = org.mockito.Mockito.mock(Post.class); + List postKeywords = keywords.stream() + .map(this::mockPostKeyword) + .toList(); + + given(post.getTitle()).willReturn(title); + given(post.getKeywords()).willReturn(postKeywords); + + return post; + } + + private PostKeyword mockPostKeyword(String keyword) { + PostKeyword postKeyword = org.mockito.Mockito.mock(PostKeyword.class); + given(postKeyword.getKeyword()).willReturn(keyword); + return postKeyword; + } +} From 5ce9065982d6a0e61d4590c0e0480eb57798a6b3 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Tue, 28 Apr 2026 21:38:24 +0900 Subject: [PATCH 2/6] =?UTF-8?q?refactor:=20=EC=9C=A0=EC=A0=80=20=EA=B3=84?= =?UTF-8?q?=EC=A0=95=20=ED=94=84=EB=A1=9C=ED=95=84=EC=9D=84=20AccountProfi?= =?UTF-8?q?le=EB=A1=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 80 +++++++++---------- .../domain/user/converter/UserConverter.java | 6 +- ...ponse.java => AccountProfileResponse.java} | 4 +- ....java => UpdateAccountProfileRequest.java} | 4 +- .../user/service/UserCommandService.java | 6 +- .../domain/user/service/UserQueryService.java | 8 +- .../UserControllerIntegrationTest.java | 24 +++--- .../user/service/UserCommandServiceTest.java | 42 +++++----- .../user/service/UserQueryServiceTest.java | 20 ++--- 9 files changed, 97 insertions(+), 97 deletions(-) rename src/main/java/com/techfork/domain/user/dto/{UserProfileResponse.java => AccountProfileResponse.java} (81%) rename src/main/java/com/techfork/domain/user/dto/{UpdateUserProfileRequest.java => UpdateAccountProfileRequest.java} (77%) diff --git a/src/main/java/com/techfork/domain/user/controller/UserController.java b/src/main/java/com/techfork/domain/user/controller/UserController.java index 530c194d..2243c11e 100644 --- a/src/main/java/com/techfork/domain/user/controller/UserController.java +++ b/src/main/java/com/techfork/domain/user/controller/UserController.java @@ -1,13 +1,13 @@ -package com.techfork.domain.user.controller; - -import com.techfork.domain.user.dto.SaveInterestRequest; -import com.techfork.domain.user.dto.UpdateUserProfileRequest; -import com.techfork.domain.user.dto.UserInterestResponse; -import com.techfork.domain.user.dto.UserProfileResponse; -import com.techfork.domain.user.service.InterestCommandService; -import com.techfork.domain.user.service.InterestQueryService; -import com.techfork.domain.user.service.UserCommandService; -import com.techfork.domain.user.service.UserQueryService; +package com.techfork.domain.user.controller; + +import com.techfork.domain.user.dto.SaveInterestRequest; +import com.techfork.domain.user.dto.UpdateAccountProfileRequest; +import com.techfork.domain.user.dto.UserInterestResponse; +import com.techfork.domain.user.dto.AccountProfileResponse; +import com.techfork.domain.user.service.InterestCommandService; +import com.techfork.domain.user.service.InterestQueryService; +import com.techfork.domain.user.service.UserCommandService; +import com.techfork.domain.user.service.UserQueryService; import com.techfork.global.common.code.SuccessCode; import com.techfork.global.response.BaseResponse; import com.techfork.global.security.oauth.UserPrincipal; @@ -55,37 +55,37 @@ public ResponseEntity> getMyInterests( ) { UserInterestResponse response = interestQueryService.getUserInterests(userPrincipal.getId()); return BaseResponse.of(SuccessCode.OK, response); - } - - @Operation( - summary = "내 프로필 수정", - description = "현재 로그인한 사용자의 프로필 정보를 수정합니다. 닉네임과 자기소개를 선택적으로 수정할 수 있습니다." - ) - @PatchMapping("/me/profile") - public ResponseEntity> updateMyProfile( - @AuthenticationPrincipal UserPrincipal userPrincipal, - @RequestBody UpdateUserProfileRequest request - ) { - userCommandService.updateUserProfile(userPrincipal.getId(), request); - return BaseResponse.of(SuccessCode.OK); - } - - @Operation( - summary = "내 프로필 조회", - description = "현재 로그인한 사용자의 프로필 정보를 조회합니다. (프로필 이미지, 닉네임, 이메일, 자기소개)" - ) - @GetMapping("/me/profile") - public ResponseEntity> getMyProfile( - @AuthenticationPrincipal UserPrincipal userPrincipal - ) { - UserProfileResponse response = userQueryService.getUserProfile(userPrincipal.getId()); - return BaseResponse.of(SuccessCode.OK, response); - } + } + + @Operation( + summary = "내 계정 프로필 수정", + description = "현재 로그인한 사용자의 계정 프로필 정보를 수정합니다. 닉네임과 자기소개를 선택적으로 수정할 수 있습니다." + ) + @PatchMapping("/me/profile") + public ResponseEntity> updateMyAccountProfile( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @RequestBody UpdateAccountProfileRequest request + ) { + userCommandService.updateAccountProfile(userPrincipal.getId(), request); + return BaseResponse.of(SuccessCode.OK); + } + + @Operation( + summary = "내 계정 프로필 조회", + description = "현재 로그인한 사용자의 계정 프로필 정보를 조회합니다. (프로필 이미지, 닉네임, 이메일, 자기소개)" + ) + @GetMapping("/me/profile") + public ResponseEntity> getMyAccountProfile( + @AuthenticationPrincipal UserPrincipal userPrincipal + ) { + AccountProfileResponse response = userQueryService.getAccountProfile(userPrincipal.getId()); + return BaseResponse.of(SuccessCode.OK, response); + } - @Operation( - summary = "회원 탈퇴", - description = "현재 로그인한 사용자의 계정을 탈퇴 처리합니다. 개인정보는 즉시 익명화되며, 활동 기록은 통계 목적으로 유지됩니다. 탈퇴 후 동일한 소셜 계정으로 재가입 시 새로운 회원으로 온보딩부터 다시 시작합니다." - ) + @Operation( + summary = "회원 탈퇴", + description = "현재 로그인한 사용자의 계정을 탈퇴 처리합니다. 계정 프로필 개인정보는 즉시 익명화되며, 활동 기록은 통계 목적으로 유지됩니다. 탈퇴 후 동일한 소셜 계정으로 재가입 시 새로운 회원으로 온보딩부터 다시 시작합니다." + ) @PatchMapping("/me/withdrawal") public ResponseEntity> withdrawUser( @AuthenticationPrincipal UserPrincipal userPrincipal diff --git a/src/main/java/com/techfork/domain/user/converter/UserConverter.java b/src/main/java/com/techfork/domain/user/converter/UserConverter.java index 0e7c71f0..026c5cd0 100644 --- a/src/main/java/com/techfork/domain/user/converter/UserConverter.java +++ b/src/main/java/com/techfork/domain/user/converter/UserConverter.java @@ -1,14 +1,14 @@ package com.techfork.domain.user.converter; -import com.techfork.domain.user.dto.UserProfileResponse; +import com.techfork.domain.user.dto.AccountProfileResponse; import com.techfork.domain.user.entity.User; import org.springframework.stereotype.Component; @Component public class UserConverter { - public UserProfileResponse toUserProfileResponse(User user) { - return UserProfileResponse.builder() + public AccountProfileResponse toAccountProfileResponse(User user) { + return AccountProfileResponse.builder() .profileImage(user.getProfileImage()) .nickName(user.getNickName()) .email(user.getEmail()) diff --git a/src/main/java/com/techfork/domain/user/dto/UserProfileResponse.java b/src/main/java/com/techfork/domain/user/dto/AccountProfileResponse.java similarity index 81% rename from src/main/java/com/techfork/domain/user/dto/UserProfileResponse.java rename to src/main/java/com/techfork/domain/user/dto/AccountProfileResponse.java index 71e35003..53bdd252 100644 --- a/src/main/java/com/techfork/domain/user/dto/UserProfileResponse.java +++ b/src/main/java/com/techfork/domain/user/dto/AccountProfileResponse.java @@ -3,10 +3,10 @@ import lombok.Builder; @Builder -public record UserProfileResponse( +public record AccountProfileResponse( String profileImage, String nickName, String email, String description ) { -} \ No newline at end of file +} diff --git a/src/main/java/com/techfork/domain/user/dto/UpdateUserProfileRequest.java b/src/main/java/com/techfork/domain/user/dto/UpdateAccountProfileRequest.java similarity index 77% rename from src/main/java/com/techfork/domain/user/dto/UpdateUserProfileRequest.java rename to src/main/java/com/techfork/domain/user/dto/UpdateAccountProfileRequest.java index e9fce44f..d26b3544 100644 --- a/src/main/java/com/techfork/domain/user/dto/UpdateUserProfileRequest.java +++ b/src/main/java/com/techfork/domain/user/dto/UpdateAccountProfileRequest.java @@ -2,8 +2,8 @@ import io.swagger.v3.oas.annotations.media.Schema; -@Schema(description = "사용자 프로필 수정 요청") -public record UpdateUserProfileRequest( +@Schema(description = "계정 프로필 수정 요청") +public record UpdateAccountProfileRequest( @Schema(description = "닉네임 (선택적)", example = "테크러버") String nickName, diff --git a/src/main/java/com/techfork/domain/user/service/UserCommandService.java b/src/main/java/com/techfork/domain/user/service/UserCommandService.java index 21efdabb..4fe3f9aa 100644 --- a/src/main/java/com/techfork/domain/user/service/UserCommandService.java +++ b/src/main/java/com/techfork/domain/user/service/UserCommandService.java @@ -2,7 +2,7 @@ import com.techfork.domain.user.dto.OnboardingRequest; import com.techfork.domain.user.dto.SaveInterestRequest; -import com.techfork.domain.user.dto.UpdateUserProfileRequest; +import com.techfork.domain.user.dto.UpdateAccountProfileRequest; import com.techfork.domain.user.entity.User; import com.techfork.domain.user.exception.UserErrorCode; import com.techfork.domain.user.repository.UserRepository; @@ -35,13 +35,13 @@ public void completeOnboarding(Long userId, @Valid OnboardingRequest request) { userAuthCacheService.evict(userId); } - public void updateUserProfile(Long userId, UpdateUserProfileRequest request) { + public void updateAccountProfile(Long userId, UpdateAccountProfileRequest request) { User user = userRepository.findById(userId) .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); user.updateProfile(request.nickName(), request.description()); - log.info("User profile updated for userId: {} - nickName: {}, description: {}", + log.info("Account profile updated for userId: {} - nickName: {}, description: {}", userId, request.nickName() != null ? "updated" : "unchanged", request.description() != null ? "updated" : "unchanged"); diff --git a/src/main/java/com/techfork/domain/user/service/UserQueryService.java b/src/main/java/com/techfork/domain/user/service/UserQueryService.java index 1b2e4da1..6a5458b1 100644 --- a/src/main/java/com/techfork/domain/user/service/UserQueryService.java +++ b/src/main/java/com/techfork/domain/user/service/UserQueryService.java @@ -1,7 +1,7 @@ package com.techfork.domain.user.service; import com.techfork.domain.user.converter.UserConverter; -import com.techfork.domain.user.dto.UserProfileResponse; +import com.techfork.domain.user.dto.AccountProfileResponse; import com.techfork.domain.user.entity.User; import com.techfork.domain.user.exception.UserErrorCode; import com.techfork.domain.user.repository.UserRepository; @@ -20,12 +20,12 @@ public class UserQueryService { private final UserRepository userRepository; private final UserConverter userConverter; - public UserProfileResponse getUserProfile(Long userId) { + public AccountProfileResponse getAccountProfile(Long userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); - log.info("User profile retrieved for userId: {}", userId); + log.info("Account profile retrieved for userId: {}", userId); - return userConverter.toUserProfileResponse(user); + return userConverter.toAccountProfileResponse(user); } } diff --git a/src/test/java/com/techfork/domain/user/controller/UserControllerIntegrationTest.java b/src/test/java/com/techfork/domain/user/controller/UserControllerIntegrationTest.java index 5c2e3bb0..f479b460 100644 --- a/src/test/java/com/techfork/domain/user/controller/UserControllerIntegrationTest.java +++ b/src/test/java/com/techfork/domain/user/controller/UserControllerIntegrationTest.java @@ -1,7 +1,7 @@ package com.techfork.domain.user.controller; import com.fasterxml.jackson.databind.ObjectMapper; -import com.techfork.domain.user.dto.UpdateUserProfileRequest; +import com.techfork.domain.user.dto.UpdateAccountProfileRequest; import com.techfork.domain.user.entity.User; import com.techfork.domain.user.enums.Role; import com.techfork.domain.user.enums.SocialType; @@ -65,7 +65,7 @@ void tearDown() { // ===== 프로필 조회 테스트 ===== @Test - @DisplayName("내 프로필 조회 성공") + @DisplayName("내 계정 프로필 조회 성공") void getMyProfile_Success() throws Exception { // When & Then mockMvc.perform(get("/api/v1/users/me/profile") @@ -82,10 +82,10 @@ void getMyProfile_Success() throws Exception { // ===== 프로필 수정 테스트 ===== @Test - @DisplayName("내 프로필 수정 성공 - 닉네임만 수정") + @DisplayName("내 계정 프로필 수정 성공 - 닉네임만 수정") void updateMyProfile_Success_OnlyNickName() throws Exception { // Given - UpdateUserProfileRequest request = new UpdateUserProfileRequest("새로운닉네임", null); + UpdateAccountProfileRequest request = new UpdateAccountProfileRequest("새로운닉네임", null); // When & Then mockMvc.perform(patch("/api/v1/users/me/profile") @@ -103,10 +103,10 @@ void updateMyProfile_Success_OnlyNickName() throws Exception { } @Test - @DisplayName("내 프로필 수정 성공 - 자기소개만 수정") + @DisplayName("내 계정 프로필 수정 성공 - 자기소개만 수정") void updateMyProfile_Success_OnlyDescription() throws Exception { // Given - UpdateUserProfileRequest request = new UpdateUserProfileRequest(null, "프론트엔드 개발자로 전향했습니다."); + UpdateAccountProfileRequest request = new UpdateAccountProfileRequest(null, "프론트엔드 개발자로 전향했습니다."); // When & Then mockMvc.perform(patch("/api/v1/users/me/profile") @@ -124,10 +124,10 @@ void updateMyProfile_Success_OnlyDescription() throws Exception { } @Test - @DisplayName("내 프로필 수정 성공 - 닉네임과 자기소개 모두 수정") + @DisplayName("내 계정 프로필 수정 성공 - 닉네임과 자기소개 모두 수정") void updateMyProfile_Success_BothFields() throws Exception { // Given - UpdateUserProfileRequest request = new UpdateUserProfileRequest("풀스택개발자", "백엔드와 프론트엔드 모두 합니다."); + UpdateAccountProfileRequest request = new UpdateAccountProfileRequest("풀스택개발자", "백엔드와 프론트엔드 모두 합니다."); // When & Then mockMvc.perform(patch("/api/v1/users/me/profile") @@ -145,10 +145,10 @@ void updateMyProfile_Success_BothFields() throws Exception { } @Test - @DisplayName("내 프로필 수정 성공 - 빈 요청 (아무것도 수정하지 않음)") + @DisplayName("내 계정 프로필 수정 성공 - 빈 요청 (아무것도 수정하지 않음)") void updateMyProfile_Success_EmptyRequest() throws Exception { // Given - UpdateUserProfileRequest request = new UpdateUserProfileRequest(null, null); + UpdateAccountProfileRequest request = new UpdateAccountProfileRequest(null, null); // When & Then mockMvc.perform(patch("/api/v1/users/me/profile") @@ -166,10 +166,10 @@ void updateMyProfile_Success_EmptyRequest() throws Exception { } @Test - @DisplayName("프로필 수정 후 조회 - 변경사항 반영 확인") + @DisplayName("계정 프로필 수정 후 조회 - 변경사항 반영 확인") void updateAndGetProfile_Success() throws Exception { // Given - UpdateUserProfileRequest updateRequest = new UpdateUserProfileRequest("변경된닉네임", "변경된 자기소개"); + UpdateAccountProfileRequest updateRequest = new UpdateAccountProfileRequest("변경된닉네임", "변경된 자기소개"); // When - 프로필 수정 mockMvc.perform(patch("/api/v1/users/me/profile") diff --git a/src/test/java/com/techfork/domain/user/service/UserCommandServiceTest.java b/src/test/java/com/techfork/domain/user/service/UserCommandServiceTest.java index 8802ec86..5dde77cf 100644 --- a/src/test/java/com/techfork/domain/user/service/UserCommandServiceTest.java +++ b/src/test/java/com/techfork/domain/user/service/UserCommandServiceTest.java @@ -2,7 +2,7 @@ import com.techfork.domain.user.dto.OnboardingRequest; import com.techfork.domain.user.dto.SaveInterestRequest; -import com.techfork.domain.user.dto.UpdateUserProfileRequest; +import com.techfork.domain.user.dto.UpdateAccountProfileRequest; import com.techfork.domain.user.dto.UserInterestDto; import com.techfork.domain.user.entity.User; import com.techfork.domain.user.enums.SocialType; @@ -205,14 +205,14 @@ void setUp() { } @Test - @DisplayName("프로필 수정 성공 - 닉네임만 수정") - void updateUserProfile_Success_OnlyNickName() { + @DisplayName("계정 프로필 수정 성공 - 닉네임만 수정") + void updateAccountProfile_Success_OnlyNickName() { // Given - UpdateUserProfileRequest request = new UpdateUserProfileRequest("새로운닉네임", null); + UpdateAccountProfileRequest request = new UpdateAccountProfileRequest("새로운닉네임", null); given(userRepository.findById(userId)).willReturn(Optional.of(testUser)); // When - userCommandService.updateUserProfile(userId, request); + userCommandService.updateAccountProfile(userId, request); // Then assertThat(testUser.getNickName()).isEqualTo("새로운닉네임"); @@ -222,14 +222,14 @@ void updateUserProfile_Success_OnlyNickName() { } @Test - @DisplayName("프로필 수정 성공 - 자기소개만 수정") - void updateUserProfile_Success_OnlyDescription() { + @DisplayName("계정 프로필 수정 성공 - 자기소개만 수정") + void updateAccountProfile_Success_OnlyDescription() { // Given - UpdateUserProfileRequest request = new UpdateUserProfileRequest(null, "새로운 자기소개"); + UpdateAccountProfileRequest request = new UpdateAccountProfileRequest(null, "새로운 자기소개"); given(userRepository.findById(userId)).willReturn(Optional.of(testUser)); // When - userCommandService.updateUserProfile(userId, request); + userCommandService.updateAccountProfile(userId, request); // Then assertThat(testUser.getNickName()).isEqualTo("테스트유저"); // 변경되지 않음 @@ -239,14 +239,14 @@ void updateUserProfile_Success_OnlyDescription() { } @Test - @DisplayName("프로필 수정 성공 - 닉네임과 자기소개 모두 수정") - void updateUserProfile_Success_BothFields() { + @DisplayName("계정 프로필 수정 성공 - 닉네임과 자기소개 모두 수정") + void updateAccountProfile_Success_BothFields() { // Given - UpdateUserProfileRequest request = new UpdateUserProfileRequest("새닉네임", "새 자기소개"); + UpdateAccountProfileRequest request = new UpdateAccountProfileRequest("새닉네임", "새 자기소개"); given(userRepository.findById(userId)).willReturn(Optional.of(testUser)); // When - userCommandService.updateUserProfile(userId, request); + userCommandService.updateAccountProfile(userId, request); // Then assertThat(testUser.getNickName()).isEqualTo("새닉네임"); @@ -256,14 +256,14 @@ void updateUserProfile_Success_BothFields() { } @Test - @DisplayName("프로필 수정 성공 - 아무것도 수정하지 않음") - void updateUserProfile_Success_NoChanges() { + @DisplayName("계정 프로필 수정 성공 - 아무것도 수정하지 않음") + void updateAccountProfile_Success_NoChanges() { // Given - UpdateUserProfileRequest request = new UpdateUserProfileRequest(null, null); + UpdateAccountProfileRequest request = new UpdateAccountProfileRequest(null, null); given(userRepository.findById(userId)).willReturn(Optional.of(testUser)); // When - userCommandService.updateUserProfile(userId, request); + userCommandService.updateAccountProfile(userId, request); // Then assertThat(testUser.getNickName()).isEqualTo("테스트유저"); // 변경되지 않음 @@ -273,14 +273,14 @@ void updateUserProfile_Success_NoChanges() { } @Test - @DisplayName("프로필 수정 실패 - 사용자를 찾을 수 없음") - void updateUserProfile_Fail_UserNotFound() { + @DisplayName("계정 프로필 수정 실패 - 사용자를 찾을 수 없음") + void updateAccountProfile_Fail_UserNotFound() { // Given - UpdateUserProfileRequest request = new UpdateUserProfileRequest("새닉네임", "새 자기소개"); + UpdateAccountProfileRequest request = new UpdateAccountProfileRequest("새닉네임", "새 자기소개"); given(userRepository.findById(userId)).willReturn(Optional.empty()); // When & Then - assertThatThrownBy(() -> userCommandService.updateUserProfile(userId, request)) + assertThatThrownBy(() -> userCommandService.updateAccountProfile(userId, request)) .isInstanceOf(GeneralException.class) .extracting(ex -> ((GeneralException) ex).getCode()) .isEqualTo(UserErrorCode.USER_NOT_FOUND); diff --git a/src/test/java/com/techfork/domain/user/service/UserQueryServiceTest.java b/src/test/java/com/techfork/domain/user/service/UserQueryServiceTest.java index 76bebea1..0c6f83dc 100644 --- a/src/test/java/com/techfork/domain/user/service/UserQueryServiceTest.java +++ b/src/test/java/com/techfork/domain/user/service/UserQueryServiceTest.java @@ -1,7 +1,7 @@ package com.techfork.domain.user.service; import com.techfork.domain.user.converter.UserConverter; -import com.techfork.domain.user.dto.UserProfileResponse; +import com.techfork.domain.user.dto.AccountProfileResponse; import com.techfork.domain.user.entity.User; import com.techfork.domain.user.enums.SocialType; import com.techfork.domain.user.exception.UserErrorCode; @@ -47,10 +47,10 @@ void setUp() { } @Test - @DisplayName("프로필 조회 성공") - void getUserProfile_Success() { + @DisplayName("계정 프로필 조회 성공") + void getAccountProfile_Success() { // Given - UserProfileResponse expectedResponse = UserProfileResponse.builder() + AccountProfileResponse expectedResponse = AccountProfileResponse.builder() .profileImage("profile.jpg") .nickName("테스트유저") .email("test@example.com") @@ -58,10 +58,10 @@ void getUserProfile_Success() { .build(); given(userRepository.findById(userId)).willReturn(Optional.of(testUser)); - given(userConverter.toUserProfileResponse(testUser)).willReturn(expectedResponse); + given(userConverter.toAccountProfileResponse(testUser)).willReturn(expectedResponse); // When - UserProfileResponse result = userQueryService.getUserProfile(userId); + AccountProfileResponse result = userQueryService.getAccountProfile(userId); // Then assertThat(result).isNotNull(); @@ -71,17 +71,17 @@ void getUserProfile_Success() { assertThat(result.description()).isEqualTo("백엔드 개발자입니다."); verify(userRepository).findById(userId); - verify(userConverter).toUserProfileResponse(testUser); + verify(userConverter).toAccountProfileResponse(testUser); } @Test - @DisplayName("프로필 조회 실패 - 사용자를 찾을 수 없음") - void getUserProfile_Fail_UserNotFound() { + @DisplayName("계정 프로필 조회 실패 - 사용자를 찾을 수 없음") + void getAccountProfile_Fail_UserNotFound() { // Given given(userRepository.findById(userId)).willReturn(Optional.empty()); // When & Then - assertThatThrownBy(() -> userQueryService.getUserProfile(userId)) + assertThatThrownBy(() -> userQueryService.getAccountProfile(userId)) .isInstanceOf(GeneralException.class) .extracting(ex -> ((GeneralException) ex).getCode()) .isEqualTo(UserErrorCode.USER_NOT_FOUND); From 8f4865a2db66c7749397431d2acdf47cf1c4e710 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Tue, 28 Apr 2026 21:43:21 +0900 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20=EA=B0=9C=EC=9D=B8=ED=99=94=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EB=84=A4=EC=9D=B4=EB=B0=8D?= =?UTF-8?q?=EC=9D=84=20PersonalizationProfile=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/LlmRecommendationService.java | 30 +- .../search/service/SearchServiceImpl.java | 16 +- ...va => PersonalizationProfileDocument.java} | 138 ++--- ...sonalizationProfileDocumentRepository.java | 9 + .../UserProfileDocumentRepository.java | 9 - ...a => PersonalizationProfileScheduler.java} | 90 +-- .../user/service/InterestCommandService.java | 154 ++--- ...ava => PersonalizationProfileService.java} | 574 +++++++++--------- .../config/ElasticsearchCacheManager.java | 4 +- .../service/InterestCommandServiceTest.java | 16 +- ...=> PersonalizationProfileServiceTest.java} | 42 +- .../RecommendationEvaluationService.java | 20 +- .../setup/UserDataSetupAndExporter.java | 28 +- .../components/GroundTruthGenerator.java | 6 +- .../setup/components/TestDataGenerator.java | 18 +- .../util/EvaluationFixtureLoader.java | 12 +- .../search/SearchEvaluationTestBase.java | 6 +- ...=> PersonalizationProfileServiceTest.java} | 8 +- .../setup/SearchGroundTruthGenerator.java | 6 +- 19 files changed, 596 insertions(+), 590 deletions(-) rename src/main/java/com/techfork/domain/user/document/{UserProfileDocument.java => PersonalizationProfileDocument.java} (86%) create mode 100644 src/main/java/com/techfork/domain/user/repository/PersonalizationProfileDocumentRepository.java delete mode 100644 src/main/java/com/techfork/domain/user/repository/UserProfileDocumentRepository.java rename src/main/java/com/techfork/domain/user/scheduler/{UserProfileScheduler.java => PersonalizationProfileScheduler.java} (62%) rename src/main/java/com/techfork/domain/user/service/{UserProfileService.java => PersonalizationProfileService.java} (89%) rename src/test/java/com/techfork/domain/user/service/{UserProfileServiceTest.java => PersonalizationProfileServiceTest.java} (85%) rename src/test/java/com/techfork/evaluation/search/setup/{UserProfileServiceTest.java => PersonalizationProfileServiceTest.java} (91%) diff --git a/src/main/java/com/techfork/domain/recommendation/service/LlmRecommendationService.java b/src/main/java/com/techfork/domain/recommendation/service/LlmRecommendationService.java index 278f412b..19adac2f 100644 --- a/src/main/java/com/techfork/domain/recommendation/service/LlmRecommendationService.java +++ b/src/main/java/com/techfork/domain/recommendation/service/LlmRecommendationService.java @@ -17,9 +17,9 @@ import com.techfork.domain.recommendation.repository.RecommendationHistoryRepository; import com.techfork.domain.recommendation.service.MmrService.MmrCandidate; import com.techfork.domain.recommendation.service.MmrService.MmrResult; -import com.techfork.domain.user.document.UserProfileDocument; +import com.techfork.domain.user.document.PersonalizationProfileDocument; import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.repository.UserProfileDocumentRepository; +import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; import com.techfork.global.util.RrfScorer; import com.techfork.global.util.TimeDecayStrategy; import com.techfork.global.util.VectorUtil; @@ -48,7 +48,7 @@ public class LlmRecommendationService implements RecommendationService { private final ElasticsearchClient elasticsearchClient; - private final UserProfileDocumentRepository userProfileDocumentRepository; + private final PersonalizationProfileDocumentRepository personalizationProfileDocumentRepository; private final RecommendedPostRepository recommendedPostRepository; private final RecommendationHistoryRepository recommendationHistoryRepository; private final ReadPostRepository readPostRepository; @@ -69,18 +69,19 @@ public class LlmRecommendationService implements RecommendationService { public int generateRecommendationsForUser(User user) { log.info("사용자 {} 추천 생성 시작", user.getId()); - Optional profileOpt = userProfileDocumentRepository.findByUserId(user.getId()); - if (profileOpt.isEmpty() || profileOpt.get().getProfileVector() == null) { - log.warn("사용자 {}의 프로필 또는 벡터를 찾을 수 없음. 추천 생성 스킵.", user.getId()); + Optional personalizationProfileOpt = + personalizationProfileDocumentRepository.findByUserId(user.getId()); + if (personalizationProfileOpt.isEmpty() || personalizationProfileOpt.get().getProfileVector() == null) { + log.warn("사용자 {}의 개인화 프로필 또는 벡터를 찾을 수 없음. 추천 생성 스킵.", user.getId()); return 0; } - UserProfileDocument profile = profileOpt.get(); - float[] userProfileVector = profile.getProfileVector(); + PersonalizationProfileDocument personalizationProfile = personalizationProfileOpt.get(); + float[] personalizationProfileVector = personalizationProfile.getProfileVector(); try { // 2. k-NN 검색으로 초기 후보군 가져오기 - List candidates = searchCandidates(userProfileVector, user); + List candidates = searchCandidates(personalizationProfileVector, user); if (candidates.isEmpty()) { log.info("사용자 {}의 추천 후보군을 찾을 수 없음", user.getId()); @@ -118,14 +119,17 @@ public int generateRecommendationsForUser(User user) { } } - private List searchCandidates(float[] userProfileVector, User user) throws IOException { + private List searchCandidates(float[] personalizationProfileVector, User user) throws IOException { Set readPostIds = readPostRepository.findRecentReadPostsByUserIdWithMinDuration(user.getId(), PageRequest.of(0, 1000)) .stream() .map(readPost -> readPost.getPost().getId()) .collect(Collectors.toSet()); - Optional profileOpt = userProfileDocumentRepository.findByUserId(user.getId()); - List keyKeywords = profileOpt.map(UserProfileDocument::getKeyKeywords).orElse(List.of()); + Optional personalizationProfileOpt = + personalizationProfileDocumentRepository.findByUserId(user.getId()); + List keyKeywords = personalizationProfileOpt + .map(PersonalizationProfileDocument::getKeyKeywords) + .orElse(List.of()); RecommendationProperties.EmbeddingWeights weights = properties.getEmbeddingWeights(); Query filterQuery = vectorQueryBuilder.createExcludeFilter(readPostIds); @@ -133,7 +137,7 @@ private List searchCandidates(float[] userProfileVector, User user // 1. kNN 검색 쿼리 준비 List knnSearches = vectorQueryBuilder.createKnnSearches( TITLE_EMBEDDING_FIELD, SUMMARY_EMBEDDING_FIELD, CONTENT_CHUNKS_EMBEDDING_FIELD, - userProfileVector, weights.getTitle(), weights.getSummary(), weights.getContent(), + personalizationProfileVector, weights.getTitle(), weights.getSummary(), weights.getContent(), properties.getKnnSearchSize(), properties.getNumCandidates(), filterQuery ); diff --git a/src/main/java/com/techfork/domain/search/service/SearchServiceImpl.java b/src/main/java/com/techfork/domain/search/service/SearchServiceImpl.java index e471b3b8..e7da61cd 100644 --- a/src/main/java/com/techfork/domain/search/service/SearchServiceImpl.java +++ b/src/main/java/com/techfork/domain/search/service/SearchServiceImpl.java @@ -13,8 +13,8 @@ import com.techfork.domain.search.config.GeneralSearchProperties; import com.techfork.domain.search.dto.SearchResult; -import com.techfork.domain.user.document.UserProfileDocument; -import com.techfork.domain.user.repository.UserProfileDocumentRepository; +import com.techfork.domain.user.document.PersonalizationProfileDocument; +import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; import com.techfork.global.llm.EmbeddingClient; import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; import com.techfork.global.util.RrfScorer; @@ -52,7 +52,7 @@ public class SearchServiceImpl implements SearchService { private final ElasticsearchClient elasticsearchClient; private final EmbeddingClient embeddingClient; private final GeneralSearchProperties generalSearchProperties; - private final UserProfileDocumentRepository userProfileDocumentRepository; + private final PersonalizationProfileDocumentRepository personalizationProfileDocumentRepository; private final PostRepository postRepository; private final BookmarkRepository bookmarkRepository; private final Executor searchAsyncExecutor; @@ -90,8 +90,10 @@ public List searchPersonalized(String query, Long userId) { log.debug("Personalized search started for userId: {} with query: '{}'", userId, query); long startTime = System.currentTimeMillis(); - Optional userProfileOpt = userProfileDocumentRepository.findByUserId(userId); - boolean hasProfile = userProfileOpt.isPresent() && userProfileOpt.get().getProfileVector() != null; + Optional personalizationProfileOpt = + personalizationProfileDocumentRepository.findByUserId(userId); + boolean hasProfile = personalizationProfileOpt.isPresent() + && personalizationProfileOpt.get().getProfileVector() != null; int candidateSize = hasProfile ? generalSearchProperties.getRRF_WINDOW_SIZE() @@ -107,8 +109,8 @@ public List searchPersonalized(String query, Long userId) { log.info("Personalized Search [FALLBACK]. UserID={}, Query='{}', Results={}, Time={}ms (Reason: No Profile)", userId, query, finalResults.size(), duration); } else { - float[] userProfileVector = userProfileOpt.get().getProfileVector(); - finalResults = personalReranking(initialResults, userProfileVector); + float[] personalizationProfileVector = personalizationProfileOpt.get().getProfileVector(); + finalResults = personalReranking(initialResults, personalizationProfileVector); long duration = System.currentTimeMillis() - startTime; log.info("Personalized Search [RERANKED]. UserID={}, Query='{}', Results={}, Time={}ms", userId, query, finalResults.size(), duration); diff --git a/src/main/java/com/techfork/domain/user/document/UserProfileDocument.java b/src/main/java/com/techfork/domain/user/document/PersonalizationProfileDocument.java similarity index 86% rename from src/main/java/com/techfork/domain/user/document/UserProfileDocument.java rename to src/main/java/com/techfork/domain/user/document/PersonalizationProfileDocument.java index b23c7743..5c494485 100644 --- a/src/main/java/com/techfork/domain/user/document/UserProfileDocument.java +++ b/src/main/java/com/techfork/domain/user/document/PersonalizationProfileDocument.java @@ -1,69 +1,69 @@ -package com.techfork.domain.user.document; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.Transient; -import org.springframework.data.elasticsearch.annotations.Document; -import org.springframework.data.elasticsearch.annotations.Field; -import org.springframework.data.elasticsearch.annotations.FieldType; - -import java.time.LocalDateTime; -import java.util.List; - -@Document(indexName = "user_profiles") -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@JsonIgnoreProperties(ignoreUnknown = true) -public class UserProfileDocument { - - @Id - private String id; // userId를 문자열로 - - @Field(type = FieldType.Long) - private Long userId; - - @Field(type = FieldType.Text) - private String profileText; - - @Field(type = FieldType.Dense_Vector, dims = 3072) // OpenAI text-embedding-3-large dimension - private float[] profileVector; - - @Field(type = FieldType.Keyword) - private List interests; - - @Field(type = FieldType.Keyword) - private List keyKeywords; - - @Field(type = FieldType.Date) - @Transient - private LocalDateTime generatedAt; - - @Builder - private UserProfileDocument(Long userId, String profileText, float[] profileVector, - List interests, List keyKeywords, LocalDateTime generatedAt) { - this.id = String.valueOf(userId); - this.userId = userId; - this.profileText = profileText; - this.profileVector = profileVector; - this.interests = interests; - this.keyKeywords = keyKeywords; - this.generatedAt = generatedAt; - } - - public static UserProfileDocument create(Long userId, String profileText, float[] profileVector, - List interests, List keyKeywords) { - return UserProfileDocument.builder() - .userId(userId) - .profileText(profileText) - .profileVector(profileVector) - .interests(interests) - .keyKeywords(keyKeywords) - .generatedAt(LocalDateTime.now()) - .build(); - } -} +package com.techfork.domain.user.document; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +import java.time.LocalDateTime; +import java.util.List; + +@Document(indexName = "user_profiles") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@JsonIgnoreProperties(ignoreUnknown = true) +public class PersonalizationProfileDocument { + + @Id + private String id; // userId를 문자열로 + + @Field(type = FieldType.Long) + private Long userId; + + @Field(type = FieldType.Text) + private String profileText; + + @Field(type = FieldType.Dense_Vector, dims = 3072) // OpenAI text-embedding-3-large dimension + private float[] profileVector; + + @Field(type = FieldType.Keyword) + private List interests; + + @Field(type = FieldType.Keyword) + private List keyKeywords; + + @Field(type = FieldType.Date) + @Transient + private LocalDateTime generatedAt; + + @Builder + private PersonalizationProfileDocument(Long userId, String profileText, float[] profileVector, + List interests, List keyKeywords, LocalDateTime generatedAt) { + this.id = String.valueOf(userId); + this.userId = userId; + this.profileText = profileText; + this.profileVector = profileVector; + this.interests = interests; + this.keyKeywords = keyKeywords; + this.generatedAt = generatedAt; + } + + public static PersonalizationProfileDocument create(Long userId, String profileText, float[] profileVector, + List interests, List keyKeywords) { + return PersonalizationProfileDocument.builder() + .userId(userId) + .profileText(profileText) + .profileVector(profileVector) + .interests(interests) + .keyKeywords(keyKeywords) + .generatedAt(LocalDateTime.now()) + .build(); + } +} diff --git a/src/main/java/com/techfork/domain/user/repository/PersonalizationProfileDocumentRepository.java b/src/main/java/com/techfork/domain/user/repository/PersonalizationProfileDocumentRepository.java new file mode 100644 index 00000000..d9365bcd --- /dev/null +++ b/src/main/java/com/techfork/domain/user/repository/PersonalizationProfileDocumentRepository.java @@ -0,0 +1,9 @@ +package com.techfork.domain.user.repository; + +import com.techfork.domain.user.document.PersonalizationProfileDocument; +import java.util.Optional; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +public interface PersonalizationProfileDocumentRepository extends ElasticsearchRepository { + Optional findByUserId(Long id); +} \ No newline at end of file diff --git a/src/main/java/com/techfork/domain/user/repository/UserProfileDocumentRepository.java b/src/main/java/com/techfork/domain/user/repository/UserProfileDocumentRepository.java deleted file mode 100644 index 69e54fc5..00000000 --- a/src/main/java/com/techfork/domain/user/repository/UserProfileDocumentRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.techfork.domain.user.repository; - -import com.techfork.domain.user.document.UserProfileDocument; -import java.util.Optional; -import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; - -public interface UserProfileDocumentRepository extends ElasticsearchRepository { - Optional findByUserId(Long id); -} \ No newline at end of file diff --git a/src/main/java/com/techfork/domain/user/scheduler/UserProfileScheduler.java b/src/main/java/com/techfork/domain/user/scheduler/PersonalizationProfileScheduler.java similarity index 62% rename from src/main/java/com/techfork/domain/user/scheduler/UserProfileScheduler.java rename to src/main/java/com/techfork/domain/user/scheduler/PersonalizationProfileScheduler.java index f2464334..8be25469 100644 --- a/src/main/java/com/techfork/domain/user/scheduler/UserProfileScheduler.java +++ b/src/main/java/com/techfork/domain/user/scheduler/PersonalizationProfileScheduler.java @@ -1,45 +1,45 @@ -package com.techfork.domain.user.scheduler; - -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.repository.UserRepository; -import com.techfork.domain.user.service.UserProfileService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import java.time.LocalDateTime; -import java.util.List; - -@Slf4j -@Component -@RequiredArgsConstructor -public class UserProfileScheduler { - - private final UserRepository userRepository; - private final UserProfileService userProfileService; - - /** - * 매일 오전 6시(KST)에 최근 24시간 내 활성 사용자의 프로필을 재생성 - * - 크롤링(5시) 후 1시간 뒤 실행 - */ - @Scheduled(cron = "0 0 6 * * *", zone = "Asia/Seoul") - public void regenerateActiveUserProfiles() { - log.info("Starting daily user profile regeneration for active users"); - - LocalDateTime since = LocalDateTime.now().minusHours(24); - List activeUsers = userRepository.findActiveUsersSince(since); - - log.info("Found {} active users in the last 24 hours", activeUsers.size()); - - activeUsers.forEach(user -> { - try { - userProfileService.generateUserProfile(user.getId()); - } catch (Exception e) { - log.error("Failed to generate profile for user: {}", user.getId(), e); - } - }); - - log.info("Completed daily user profile regeneration for {} active users", activeUsers.size()); - } -} +package com.techfork.domain.user.scheduler; + +import com.techfork.domain.user.entity.User; +import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.user.service.PersonalizationProfileService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class PersonalizationProfileScheduler { + + private final UserRepository userRepository; + private final PersonalizationProfileService personalizationProfileService; + + /** + * 매일 오전 6시(KST)에 최근 24시간 내 활성 사용자의 개인화 프로필을 재생성 + * - 크롤링(5시) 후 1시간 뒤 실행 + */ + @Scheduled(cron = "0 0 6 * * *", zone = "Asia/Seoul") + public void regenerateActiveUserProfiles() { + log.info("Starting daily personalization profile regeneration for active users"); + + LocalDateTime since = LocalDateTime.now().minusHours(24); + List activeUsers = userRepository.findActiveUsersSince(since); + + log.info("Found {} active users in the last 24 hours", activeUsers.size()); + + activeUsers.forEach(user -> { + try { + personalizationProfileService.generatePersonalizationProfile(user.getId()); + } catch (Exception e) { + log.error("Failed to generate personalization profile for user: {}", user.getId(), e); + } + }); + + log.info("Completed daily personalization profile regeneration for {} active users", activeUsers.size()); + } +} diff --git a/src/main/java/com/techfork/domain/user/service/InterestCommandService.java b/src/main/java/com/techfork/domain/user/service/InterestCommandService.java index 7896739b..1a15f57b 100644 --- a/src/main/java/com/techfork/domain/user/service/InterestCommandService.java +++ b/src/main/java/com/techfork/domain/user/service/InterestCommandService.java @@ -1,77 +1,77 @@ -package com.techfork.domain.user.service; - -import com.techfork.domain.user.dto.SaveInterestRequest; -import com.techfork.domain.user.dto.UserInterestDto; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.entity.UserInterestCategory; -import com.techfork.domain.user.entity.UserInterestKeyword; -import com.techfork.domain.user.enums.EInterestCategory; -import com.techfork.domain.user.enums.EInterestKeyword; -import com.techfork.domain.user.exception.UserErrorCode; -import com.techfork.domain.user.repository.UserRepository; -import com.techfork.global.exception.GeneralException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional -public class InterestCommandService { - - private final UserRepository userRepository; - private final UserProfileService userProfileService; - - public void updateUserInterests(Long userId, SaveInterestRequest request) { - User user = userRepository.findByIdWithInterestCategories(userId) - .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); - - saveUserInterests(user, request); - } - - void saveUserInterests(User user, SaveInterestRequest request) { - user.getInterestCategories().clear(); - List categories = createCategoriesFromRequest(user, request); - user.getInterestCategories().addAll(categories); - - log.info("Saved {} interest categories for user {}", categories.size(), user.getId()); - - userProfileService.generateUserProfile(user.getId()); - } - - private List createCategoriesFromRequest(User user, SaveInterestRequest request) { - return request.interests().stream() - .map(dto -> createCategoryWithKeywords(user, dto)) - .toList(); - } - - private UserInterestCategory createCategoryWithKeywords(User user, UserInterestDto dto) { - EInterestCategory category = EInterestCategory.valueOf(dto.category()); - UserInterestCategory userCategory = UserInterestCategory.create(user, category); - - if (dto.keywords() != null && !dto.keywords().isEmpty()) { - addKeywordsToCategory(userCategory, category, dto.keywords()); - } - - return userCategory; - } - - private void addKeywordsToCategory(UserInterestCategory userCategory, EInterestCategory category, List keywordNames) { - for (String keywordName : keywordNames) { - EInterestKeyword keyword = EInterestKeyword.valueOf(keywordName); - validateKeywordCategory(keyword, category); - UserInterestKeyword userInterestKeyword = UserInterestKeyword.create(userCategory, keyword); - userCategory.addKeyword(userInterestKeyword); - } - } - - private void validateKeywordCategory(EInterestKeyword keyword, EInterestCategory category) { - if (keyword.getCategory() != category) { - throw new GeneralException(UserErrorCode.INVALID_INTEREST_KEYWORD); - } - } -} +package com.techfork.domain.user.service; + +import com.techfork.domain.user.dto.SaveInterestRequest; +import com.techfork.domain.user.dto.UserInterestDto; +import com.techfork.domain.user.entity.User; +import com.techfork.domain.user.entity.UserInterestCategory; +import com.techfork.domain.user.entity.UserInterestKeyword; +import com.techfork.domain.user.enums.EInterestCategory; +import com.techfork.domain.user.enums.EInterestKeyword; +import com.techfork.domain.user.exception.UserErrorCode; +import com.techfork.domain.user.repository.UserRepository; +import com.techfork.global.exception.GeneralException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class InterestCommandService { + + private final UserRepository userRepository; + private final PersonalizationProfileService personalizationProfileService; + + public void updateUserInterests(Long userId, SaveInterestRequest request) { + User user = userRepository.findByIdWithInterestCategories(userId) + .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); + + saveUserInterests(user, request); + } + + void saveUserInterests(User user, SaveInterestRequest request) { + user.getInterestCategories().clear(); + List categories = createCategoriesFromRequest(user, request); + user.getInterestCategories().addAll(categories); + + log.info("Saved {} interest categories for user {}", categories.size(), user.getId()); + + personalizationProfileService.generatePersonalizationProfile(user.getId()); + } + + private List createCategoriesFromRequest(User user, SaveInterestRequest request) { + return request.interests().stream() + .map(dto -> createCategoryWithKeywords(user, dto)) + .toList(); + } + + private UserInterestCategory createCategoryWithKeywords(User user, UserInterestDto dto) { + EInterestCategory category = EInterestCategory.valueOf(dto.category()); + UserInterestCategory userCategory = UserInterestCategory.create(user, category); + + if (dto.keywords() != null && !dto.keywords().isEmpty()) { + addKeywordsToCategory(userCategory, category, dto.keywords()); + } + + return userCategory; + } + + private void addKeywordsToCategory(UserInterestCategory userCategory, EInterestCategory category, List keywordNames) { + for (String keywordName : keywordNames) { + EInterestKeyword keyword = EInterestKeyword.valueOf(keywordName); + validateKeywordCategory(keyword, category); + UserInterestKeyword userInterestKeyword = UserInterestKeyword.create(userCategory, keyword); + userCategory.addKeyword(userInterestKeyword); + } + } + + private void validateKeywordCategory(EInterestKeyword keyword, EInterestCategory category) { + if (keyword.getCategory() != category) { + throw new GeneralException(UserErrorCode.INVALID_INTEREST_KEYWORD); + } + } +} diff --git a/src/main/java/com/techfork/domain/user/service/UserProfileService.java b/src/main/java/com/techfork/domain/user/service/PersonalizationProfileService.java similarity index 89% rename from src/main/java/com/techfork/domain/user/service/UserProfileService.java rename to src/main/java/com/techfork/domain/user/service/PersonalizationProfileService.java index c7f2002a..551039e3 100644 --- a/src/main/java/com/techfork/domain/user/service/UserProfileService.java +++ b/src/main/java/com/techfork/domain/user/service/PersonalizationProfileService.java @@ -1,304 +1,304 @@ -package com.techfork.domain.user.service; - -import com.techfork.domain.activity.entity.ReadPost; -import com.techfork.domain.activity.entity.Bookmark; -import com.techfork.domain.activity.entity.SearchHistory; -import com.techfork.domain.activity.repository.ReadPostRepository; -import com.techfork.domain.activity.repository.BookmarkRepository; -import com.techfork.domain.activity.repository.SearchHistoryRepository; -import com.techfork.domain.post.entity.PostKeyword; -import com.techfork.domain.recommendation.service.RecommendationService; -import com.techfork.domain.user.document.UserProfileDocument; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.entity.UserInterestCategory; -import com.techfork.domain.user.exception.UserErrorCode; -import com.techfork.domain.user.repository.UserInterestCategoryRepository; -import com.techfork.domain.user.repository.UserProfileDocumentRepository; -import com.techfork.domain.user.repository.UserRepository; -import com.techfork.global.exception.GeneralException; -import com.techfork.global.llm.EmbeddingClient; -import com.techfork.global.llm.LlmClient; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.PageRequest; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -@Slf4j -@Service -@RequiredArgsConstructor -public class UserProfileService { - - private final UserInterestCategoryRepository userInterestCategoryRepository; - private final ReadPostRepository readPostRepository; - private final BookmarkRepository bookmarkRepository; - private final SearchHistoryRepository searchHistoryRepository; - private final UserProfileDocumentRepository userProfileDocumentRepository; - private final UserRepository userRepository; - private final RecommendationService recommendationService; - private final LlmClient llmClient; - private final EmbeddingClient embeddingClient; - - @Async - @Transactional - public void generateUserProfile(Long userId) { - generateUserProfileSync(userId); - } - - /** - * 사용자 프로필 생성 (동기 버전) - * 테스트 환경이나 동기 실행이 필요한 경우 사용 - */ - @Transactional - public void generateUserProfileSync(Long userId) { - try { - UserActivityData activityData = collectUserActivityData(userId); - String llmResponse = generateProfileTextWithLLM(activityData); - - ProfileAndKeywords parsed = parseProfileAndKeywords(llmResponse); - float[] profileVector = generateEmbeddingVector(parsed.profileText); - - UserProfileDocument profileDocument = UserProfileDocument.create( - userId, - parsed.profileText, - profileVector, - activityData.interests, - parsed.keyKeywords - ); - - userProfileDocumentRepository.save(profileDocument); - - log.info("User profile generated successfully for userId: {}", userId); - - generateRecommendationsAfterProfile(userId); - - } catch (Exception e) { - log.error("Failed to generate user profile for userId: {}", userId, e); - throw e; - } - } - - /** - * 프로필 생성 완료 후 추천 생성 - * 온보딩 또는 관심사 변경 시 새로운 프로필 기반으로 추천을 갱신합니다. - */ - private void generateRecommendationsAfterProfile(Long userId) { - try { - User user = userRepository.findById(userId) - .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); - - int recommendationCount = recommendationService.generateRecommendationsForUser(user); - - log.info("Recommendations generated after profile creation for userId: {} - {} recommendations created", - userId, recommendationCount); - - } catch (Exception e) { - log.error("Failed to generate recommendations after profile creation for userId: {}", userId, e); - } - } - - private UserActivityData collectUserActivityData(Long userId) { - List categories = userInterestCategoryRepository.findByUserIdWithKeywords(userId); - List interests = categories.stream() - .flatMap(c -> c.getKeywords().stream()) - .map(k -> k.getKeyword().getDisplayName()) - .toList(); - - List readPosts = readPostRepository.findRecentReadPostsByUserIdWithMinDuration(userId, PageRequest.of(0, 20)); - List readPostData = readPosts.stream() - .map(rp -> new PostData( - rp.getPost().getTitle(), - rp.getPost().getKeywords().stream() - .map(PostKeyword::getKeyword) - .toList(), - convertReadingDurationToNaturalLanguage(rp.getReadDurationSeconds()) - )) - .toList(); - - List bookmarks = bookmarkRepository.findRecentBookmarksByUserId(userId, PageRequest.of(0, 20)); - List bookmarkedPostData = bookmarks.stream() - .map(sp -> new PostData( - sp.getPost().getTitle(), - sp.getPost().getKeywords().stream() - .map(PostKeyword::getKeyword) - .toList(), - null - )) - .toList(); - +package com.techfork.domain.user.service; + +import com.techfork.domain.activity.entity.ReadPost; +import com.techfork.domain.activity.entity.Bookmark; +import com.techfork.domain.activity.entity.SearchHistory; +import com.techfork.domain.activity.repository.ReadPostRepository; +import com.techfork.domain.activity.repository.BookmarkRepository; +import com.techfork.domain.activity.repository.SearchHistoryRepository; +import com.techfork.domain.post.entity.PostKeyword; +import com.techfork.domain.recommendation.service.RecommendationService; +import com.techfork.domain.user.document.PersonalizationProfileDocument; +import com.techfork.domain.user.entity.User; +import com.techfork.domain.user.entity.UserInterestCategory; +import com.techfork.domain.user.exception.UserErrorCode; +import com.techfork.domain.user.repository.UserInterestCategoryRepository; +import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; +import com.techfork.domain.user.repository.UserRepository; +import com.techfork.global.exception.GeneralException; +import com.techfork.global.llm.EmbeddingClient; +import com.techfork.global.llm.LlmClient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PersonalizationProfileService { + + private final UserInterestCategoryRepository userInterestCategoryRepository; + private final ReadPostRepository readPostRepository; + private final BookmarkRepository bookmarkRepository; + private final SearchHistoryRepository searchHistoryRepository; + private final PersonalizationProfileDocumentRepository personalizationProfileDocumentRepository; + private final UserRepository userRepository; + private final RecommendationService recommendationService; + private final LlmClient llmClient; + private final EmbeddingClient embeddingClient; + + @Async + @Transactional + public void generatePersonalizationProfile(Long userId) { + generatePersonalizationProfileSync(userId); + } + + /** + * 개인화 프로필 생성 (동기 버전) + * 테스트 환경이나 동기 실행이 필요한 경우 사용 + */ + @Transactional + public void generatePersonalizationProfileSync(Long userId) { + try { + UserActivityData activityData = collectUserActivityData(userId); + String llmResponse = generateProfileTextWithLLM(activityData); + + ProfileAndKeywords parsed = parseProfileAndKeywords(llmResponse); + float[] profileVector = generateEmbeddingVector(parsed.profileText); + + PersonalizationProfileDocument profileDocument = PersonalizationProfileDocument.create( + userId, + parsed.profileText, + profileVector, + activityData.interests, + parsed.keyKeywords + ); + + personalizationProfileDocumentRepository.save(profileDocument); + + log.info("Personalization profile generated successfully for userId: {}", userId); + + generateRecommendationsAfterProfile(userId); + + } catch (Exception e) { + log.error("Failed to generate personalization profile for userId: {}", userId, e); + throw e; + } + } + + /** + * 개인화 프로필 생성 완료 후 추천 생성 + * 온보딩 또는 관심사 변경 시 새 개인화 프로필 기반으로 추천을 갱신합니다. + */ + private void generateRecommendationsAfterProfile(Long userId) { + try { + User user = userRepository.findById(userId) + .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); + + int recommendationCount = recommendationService.generateRecommendationsForUser(user); + + log.info("Recommendations generated after personalization profile creation for userId: {} - {} recommendations created", + userId, recommendationCount); + + } catch (Exception e) { + log.error("Failed to generate recommendations after personalization profile creation for userId: {}", userId, e); + } + } + + private UserActivityData collectUserActivityData(Long userId) { + List categories = userInterestCategoryRepository.findByUserIdWithKeywords(userId); + List interests = categories.stream() + .flatMap(c -> c.getKeywords().stream()) + .map(k -> k.getKeyword().getDisplayName()) + .toList(); + + List readPosts = readPostRepository.findRecentReadPostsByUserIdWithMinDuration(userId, PageRequest.of(0, 20)); + List readPostData = readPosts.stream() + .map(rp -> new PostData( + rp.getPost().getTitle(), + rp.getPost().getKeywords().stream() + .map(PostKeyword::getKeyword) + .toList(), + convertReadingDurationToNaturalLanguage(rp.getReadDurationSeconds()) + )) + .toList(); + + List bookmarks = bookmarkRepository.findRecentBookmarksByUserId(userId, PageRequest.of(0, 20)); + List bookmarkedPostData = bookmarks.stream() + .map(sp -> new PostData( + sp.getPost().getTitle(), + sp.getPost().getKeywords().stream() + .map(PostKeyword::getKeyword) + .toList(), + null + )) + .toList(); + List searchHistories = searchHistoryRepository.findRecentSearchHistoriesByUserId(userId, PageRequest.of(0, 30)); List searchQueries = searchHistories.stream() .map(SearchHistory::getQuery) .toList(); return new UserActivityData(interests, readPostData, bookmarkedPostData, searchQueries); - } - - private String generateProfileTextWithLLM(UserActivityData data) { - String systemPrompt = "당신은 테크 블로그 플랫폼의 사용자 프로필 분석 전문가입니다. 사용자의 활동 데이터를 분석하여 검색 고도화와 포스트 추천에 최적화된 프로필을 생성합니다."; - String userPrompt = buildProfileGenerationPrompt(data); - return llmClient.call(systemPrompt, userPrompt); - } - - private String buildProfileGenerationPrompt(UserActivityData data) { - return String.format(""" - 아래 사용자의 활동 데이터를 분석하여 검색 리랭킹과 포스트 추천에 최적화된 프로필을 생성해주세요. - - ## 사용자 데이터 - - ### 관심 기술 스택 및 분야 - %s - - ### 최근 읽은 포스트 - %s - - ### 스크랩한 포스트 - %s - - ### 검색 기록 - %s - - ## 요구사항 - - 반드시 아래 형식으로 응답해주세요: - - ### PROFILE - 사용자의 기술적 관심사, 학습 패턴, 선호도를 의미 밀도 높고 풍부하게 표현한 텍스트를 작성하세요 (200-300자 정도). - - 다음 내용을 모두 포함하되 자연스러운 문장으로 작성: - 1. 주요 관심 기술 스택과 개발 분야 (백엔드/프론트엔드/인프라/AI 등) - 2. 선호하는 주제와 학습 방향 (아키텍처 설계, 성능 최적화, 트러블슈팅, 신기술 탐구 등) - 3. 읽은 포스트와 검색 기록에서 드러나는 구체적인 관심사 - 4. 현재 해결하려는 문제나 학습 중인 영역 - 5. 콘텐츠 선호 패턴 (심화 기술, 실전 경험, 튜토리얼 등) - - 주의사항: - - 마크다운 없이 순수 텍스트로만 작성 (볼드, 이탤릭, 리스트, 번호 금지) - - 구체적인 기술 용어를 많이 사용하여 임베딩 품질 향상 - - "관심이 있습니다", "선호합니다" 같은 메타 표현 대신 직접적인 기술 용어 나열 - - ### KEYWORDS - 사용자의 현재 관심사를 가장 잘 대표하는 핵심 키워드 3-5개를 쉼표로 구분하여 나열하세요. - - 구체적이고 검색 의도가 명확한 키워드만 선택 - - BM25 검색에 사용되므로 검색어로 자주 쓰일 만한 용어 선택 - - 예: Kubernetes, React hooks, 분산 트랜잭션, 성능 최적화, MSA - - 영문과 한글 혼용 가능 - - 데이터가 부족한 경우 관심 기술 스택을 기반으로 일반적인 프로필을 생성해주세요. - """, + } + + private String generateProfileTextWithLLM(UserActivityData data) { + String systemPrompt = "당신은 테크 블로그 플랫폼의 사용자 프로필 분석 전문가입니다. 사용자의 활동 데이터를 분석하여 검색 고도화와 포스트 추천에 최적화된 프로필을 생성합니다."; + String userPrompt = buildProfileGenerationPrompt(data); + return llmClient.call(systemPrompt, userPrompt); + } + + private String buildProfileGenerationPrompt(UserActivityData data) { + return String.format(""" + 아래 사용자의 활동 데이터를 분석하여 검색 리랭킹과 포스트 추천에 최적화된 프로필을 생성해주세요. + + ## 사용자 데이터 + + ### 관심 기술 스택 및 분야 + %s + + ### 최근 읽은 포스트 + %s + + ### 스크랩한 포스트 + %s + + ### 검색 기록 + %s + + ## 요구사항 + + 반드시 아래 형식으로 응답해주세요: + + ### PROFILE + 사용자의 기술적 관심사, 학습 패턴, 선호도를 의미 밀도 높고 풍부하게 표현한 텍스트를 작성하세요 (200-300자 정도). + + 다음 내용을 모두 포함하되 자연스러운 문장으로 작성: + 1. 주요 관심 기술 스택과 개발 분야 (백엔드/프론트엔드/인프라/AI 등) + 2. 선호하는 주제와 학습 방향 (아키텍처 설계, 성능 최적화, 트러블슈팅, 신기술 탐구 등) + 3. 읽은 포스트와 검색 기록에서 드러나는 구체적인 관심사 + 4. 현재 해결하려는 문제나 학습 중인 영역 + 5. 콘텐츠 선호 패턴 (심화 기술, 실전 경험, 튜토리얼 등) + + 주의사항: + - 마크다운 없이 순수 텍스트로만 작성 (볼드, 이탤릭, 리스트, 번호 금지) + - 구체적인 기술 용어를 많이 사용하여 임베딩 품질 향상 + - "관심이 있습니다", "선호합니다" 같은 메타 표현 대신 직접적인 기술 용어 나열 + + ### KEYWORDS + 사용자의 현재 관심사를 가장 잘 대표하는 핵심 키워드 3-5개를 쉼표로 구분하여 나열하세요. + - 구체적이고 검색 의도가 명확한 키워드만 선택 + - BM25 검색에 사용되므로 검색어로 자주 쓰일 만한 용어 선택 + - 예: Kubernetes, React hooks, 분산 트랜잭션, 성능 최적화, MSA + - 영문과 한글 혼용 가능 + + 데이터가 부족한 경우 관심 기술 스택을 기반으로 일반적인 프로필을 생성해주세요. + """, formatList(data.interests), formatPostDataList(data.readPostData), formatPostDataList(data.bookmarkedPostData), formatList(data.searchQueries) ); - } - - private String formatList(List items) { - if (items == null || items.isEmpty()) { - return "- (데이터 없음)"; - } - return items.stream() - .map(item -> "- " + item) - .collect(Collectors.joining("\n")); - } - - private String formatPostDataList(List postDataList) { - if (postDataList == null || postDataList.isEmpty()) { - return "- (데이터 없음)"; - } - return postDataList.stream() - .map(postData -> { - String keywordsStr = postData.keywords.isEmpty() - ? "" - : " [키워드: " + String.join(", ", postData.keywords) + "]"; - String engagementStr = postData.readingEngagement != null - ? " (" + postData.readingEngagement + ")" - : ""; - return "- " + postData.title + keywordsStr + engagementStr; - }) - .collect(Collectors.joining("\n")); - } - - private float[] generateEmbeddingVector(String profileText) { - List embedding = embeddingClient.embed(profileText); - - float[] vector = new float[embedding.size()]; - for (int i = 0; i < embedding.size(); i++) { - vector[i] = embedding.get(i); - } - return vector; - } - - private String convertReadingDurationToNaturalLanguage(Integer durationSeconds) { - if (durationSeconds == null) { - return "읽음"; - } - - if (durationSeconds <= 30) { - return "가볍게 훑어봄"; - } else if (durationSeconds <= 90) { - return "빠르게 읽음"; - } else if (durationSeconds <= 300) { - return "읽음"; - } else if (durationSeconds <= 600) { - return "정독함"; - } else { - return "깊게 읽음"; - } - } - - private ProfileAndKeywords parseProfileAndKeywords(String llmResponse) { - String profileText = ""; - List keyKeywords = List.of(); - - try { - // PROFILE 섹션 추출 - int profileStart = llmResponse.indexOf("### PROFILE"); - int keywordsStart = llmResponse.indexOf("### KEYWORDS"); - - if (profileStart != -1 && keywordsStart != -1) { - profileText = llmResponse.substring(profileStart + "### PROFILE".length(), keywordsStart) - .trim(); - - String keywordsSection = llmResponse.substring(keywordsStart + "### KEYWORDS".length()) - .trim(); - - // 쉼표로 구분된 키워드 파싱 - keyKeywords = Arrays.stream(keywordsSection.split(",")) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .limit(5) // 최대 5개 - .toList(); - } else { - // 파싱 실패 시 전체 텍스트를 프로필로 사용 - log.warn("Failed to parse LLM response sections, using full text as profile"); - profileText = llmResponse; - } - } catch (Exception e) { - log.error("Error parsing LLM response", e); - profileText = llmResponse; - } - - return new ProfileAndKeywords(profileText, keyKeywords); - } - - private record ProfileAndKeywords(String profileText, List keyKeywords) {} - + } + + private String formatList(List items) { + if (items == null || items.isEmpty()) { + return "- (데이터 없음)"; + } + return items.stream() + .map(item -> "- " + item) + .collect(Collectors.joining("\n")); + } + + private String formatPostDataList(List postDataList) { + if (postDataList == null || postDataList.isEmpty()) { + return "- (데이터 없음)"; + } + return postDataList.stream() + .map(postData -> { + String keywordsStr = postData.keywords.isEmpty() + ? "" + : " [키워드: " + String.join(", ", postData.keywords) + "]"; + String engagementStr = postData.readingEngagement != null + ? " (" + postData.readingEngagement + ")" + : ""; + return "- " + postData.title + keywordsStr + engagementStr; + }) + .collect(Collectors.joining("\n")); + } + + private float[] generateEmbeddingVector(String profileText) { + List embedding = embeddingClient.embed(profileText); + + float[] vector = new float[embedding.size()]; + for (int i = 0; i < embedding.size(); i++) { + vector[i] = embedding.get(i); + } + return vector; + } + + private String convertReadingDurationToNaturalLanguage(Integer durationSeconds) { + if (durationSeconds == null) { + return "읽음"; + } + + if (durationSeconds <= 30) { + return "가볍게 훑어봄"; + } else if (durationSeconds <= 90) { + return "빠르게 읽음"; + } else if (durationSeconds <= 300) { + return "읽음"; + } else if (durationSeconds <= 600) { + return "정독함"; + } else { + return "깊게 읽음"; + } + } + + private ProfileAndKeywords parseProfileAndKeywords(String llmResponse) { + String profileText = ""; + List keyKeywords = List.of(); + + try { + // PROFILE 섹션 추출 + int profileStart = llmResponse.indexOf("### PROFILE"); + int keywordsStart = llmResponse.indexOf("### KEYWORDS"); + + if (profileStart != -1 && keywordsStart != -1) { + profileText = llmResponse.substring(profileStart + "### PROFILE".length(), keywordsStart) + .trim(); + + String keywordsSection = llmResponse.substring(keywordsStart + "### KEYWORDS".length()) + .trim(); + + // 쉼표로 구분된 키워드 파싱 + keyKeywords = Arrays.stream(keywordsSection.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .limit(5) // 최대 5개 + .toList(); + } else { + // 파싱 실패 시 전체 텍스트를 프로필로 사용 + log.warn("Failed to parse LLM response sections, using full text as profile"); + profileText = llmResponse; + } + } catch (Exception e) { + log.error("Error parsing LLM response", e); + profileText = llmResponse; + } + + return new ProfileAndKeywords(profileText, keyKeywords); + } + + private record ProfileAndKeywords(String profileText, List keyKeywords) {} + private record UserActivityData( List interests, List readPostData, List bookmarkedPostData, List searchQueries ) {} - - private record PostData( - String title, - List keywords, - String readingEngagement - ) {} -} + + private record PostData( + String title, + List keywords, + String readingEngagement + ) {} +} diff --git a/src/main/java/com/techfork/global/config/ElasticsearchCacheManager.java b/src/main/java/com/techfork/global/config/ElasticsearchCacheManager.java index 8e4d50f5..3b364f40 100644 --- a/src/main/java/com/techfork/global/config/ElasticsearchCacheManager.java +++ b/src/main/java/com/techfork/global/config/ElasticsearchCacheManager.java @@ -5,7 +5,7 @@ import co.elastic.clients.json.JsonData; import com.techfork.domain.post.document.PostDocument; import com.techfork.domain.recommendation.config.RecommendationProperties; -import com.techfork.domain.user.document.UserProfileDocument; +import com.techfork.domain.user.document.PersonalizationProfileDocument; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; @@ -103,7 +103,7 @@ private void warmupUserProfilesKnn() { .index(USER_PROFILES_INDEX) .size(1) .knn(List.of(createKnn("profileVector", dummyVector))), - UserProfileDocument.class + PersonalizationProfileDocument.class ); log.debug("[ES Warmup] user_profiles kNN warmup OK"); } catch (Exception e) { diff --git a/src/test/java/com/techfork/domain/user/service/InterestCommandServiceTest.java b/src/test/java/com/techfork/domain/user/service/InterestCommandServiceTest.java index 264566a5..edca7343 100644 --- a/src/test/java/com/techfork/domain/user/service/InterestCommandServiceTest.java +++ b/src/test/java/com/techfork/domain/user/service/InterestCommandServiceTest.java @@ -34,7 +34,7 @@ class InterestCommandServiceTest { private UserRepository userRepository; @Mock - private UserProfileService userProfileService; + private PersonalizationProfileService personalizationProfileService; @InjectMocks private InterestCommandService interestCommandService; @@ -61,7 +61,7 @@ void saveUserInterests_Success() { assertThat(user.getInterestCategories()).hasSize(1); assertThat(user.getInterestCategories().get(0).getKeywords()).hasSize(2); - verify(userProfileService, times(1)).generateUserProfile(user.getId()); + verify(personalizationProfileService, times(1)).generatePersonalizationProfile(user.getId()); } @Test @@ -86,7 +86,7 @@ void saveUserInterests_CategoryOnly_Success() { assertThat(user.getInterestCategories()).hasSize(1); assertThat(user.getInterestCategories().get(0).getKeywords()).isEmpty(); - verify(userProfileService, times(1)).generateUserProfile(user.getId()); + verify(personalizationProfileService, times(1)).generatePersonalizationProfile(user.getId()); } @Test @@ -121,7 +121,7 @@ void saveUserInterests_MultipleCategories_Success() { assertThat(user.getInterestCategories().get(1).getKeywords()).hasSize(2); assertThat(user.getInterestCategories().get(2).getKeywords()).hasSize(3); - verify(userProfileService, times(1)).generateUserProfile(user.getId()); + verify(personalizationProfileService, times(1)).generatePersonalizationProfile(user.getId()); } @Test @@ -150,7 +150,7 @@ void saveUserInterests_ClearExistingInterests_Success() { // Then assertThat(user.getInterestCategories()).hasSize(1); - verify(userProfileService, times(1)).generateUserProfile(user.getId()); + verify(personalizationProfileService, times(1)).generatePersonalizationProfile(user.getId()); } @Test @@ -174,7 +174,7 @@ void saveUserInterests_InvalidKeywordCategory_ThrowsException() { .isInstanceOf(GeneralException.class) .hasFieldOrPropertyWithValue("code", UserErrorCode.INVALID_INTEREST_KEYWORD); - verify(userProfileService, never()).generateUserProfile(any()); + verify(personalizationProfileService, never()).generatePersonalizationProfile(any()); } @Test @@ -204,7 +204,7 @@ void updateUserInterests_Success() { assertThat(mockUser.getInterestCategories()).hasSize(1); verify(userRepository, times(1)).findByIdWithInterestCategories(userId); - verify(userProfileService, times(1)).generateUserProfile(userId); + verify(personalizationProfileService, times(1)).generatePersonalizationProfile(userId); } @Test @@ -229,6 +229,6 @@ void updateUserInterests_UserNotFound_ThrowsException() { .isInstanceOf(GeneralException.class) .hasFieldOrPropertyWithValue("code", UserErrorCode.USER_NOT_FOUND); - verify(userProfileService, never()).generateUserProfile(any()); + verify(personalizationProfileService, never()).generatePersonalizationProfile(any()); } } diff --git a/src/test/java/com/techfork/domain/user/service/UserProfileServiceTest.java b/src/test/java/com/techfork/domain/user/service/PersonalizationProfileServiceTest.java similarity index 85% rename from src/test/java/com/techfork/domain/user/service/UserProfileServiceTest.java rename to src/test/java/com/techfork/domain/user/service/PersonalizationProfileServiceTest.java index d3eadf65..75ab76bb 100644 --- a/src/test/java/com/techfork/domain/user/service/UserProfileServiceTest.java +++ b/src/test/java/com/techfork/domain/user/service/PersonalizationProfileServiceTest.java @@ -9,7 +9,7 @@ import com.techfork.domain.post.entity.Post; import com.techfork.domain.post.entity.PostKeyword; import com.techfork.domain.recommendation.service.RecommendationService; -import com.techfork.domain.user.document.UserProfileDocument; +import com.techfork.domain.user.document.PersonalizationProfileDocument; import com.techfork.domain.user.entity.User; import com.techfork.domain.user.entity.UserInterestCategory; import com.techfork.domain.user.entity.UserInterestKeyword; @@ -17,7 +17,7 @@ import com.techfork.domain.user.enums.EInterestKeyword; import com.techfork.domain.user.enums.SocialType; import com.techfork.domain.user.repository.UserInterestCategoryRepository; -import com.techfork.domain.user.repository.UserProfileDocumentRepository; +import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; import com.techfork.domain.user.repository.UserRepository; import com.techfork.global.llm.EmbeddingClient; import com.techfork.global.llm.LlmClient; @@ -43,7 +43,7 @@ import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) -class UserProfileServiceTest { +class PersonalizationProfileServiceTest { @Mock private UserInterestCategoryRepository userInterestCategoryRepository; @@ -58,7 +58,7 @@ class UserProfileServiceTest { private SearchHistoryRepository searchHistoryRepository; @Mock - private UserProfileDocumentRepository userProfileDocumentRepository; + private PersonalizationProfileDocumentRepository personalizationProfileDocumentRepository; @Mock private UserRepository userRepository; @@ -73,11 +73,11 @@ class UserProfileServiceTest { private EmbeddingClient embeddingClient; @InjectMocks - private UserProfileService userProfileService; + private PersonalizationProfileService personalizationProfileService; @Test @DisplayName("사용자 활동 데이터를 모아 개인화 프로필을 생성하고 저장한다") - void generateUserProfileSync_CollectsActivityDataParsesAndSavesProfile() { + void generatePersonalizationProfileSync_CollectsActivityDataParsesAndSavesProfile() { Long userId = 1L; User user = createUser(userId); List readPosts = List.of( @@ -115,12 +115,12 @@ void generateUserProfileSync_CollectsActivityDataParsesAndSavesProfile() { Java, Spring, Docker, Elasticsearch, Batch """); given(embeddingClient.embed(anyString())).willReturn(List.of(0.1f, 0.2f, 0.3f)); - given(userProfileDocumentRepository.save(any(UserProfileDocument.class))) + given(personalizationProfileDocumentRepository.save(any(PersonalizationProfileDocument.class))) .willAnswer(invocation -> invocation.getArgument(0)); given(userRepository.findById(userId)).willReturn(Optional.of(user)); given(recommendationService.generateRecommendationsForUser(user)).willReturn(5); - userProfileService.generateUserProfileSync(userId); + personalizationProfileService.generatePersonalizationProfileSync(userId); ArgumentCaptor promptCaptor = ArgumentCaptor.forClass(String.class); verify(llmClient).call(anyString(), promptCaptor.capture()); @@ -145,10 +145,10 @@ void generateUserProfileSync_CollectsActivityDataParsesAndSavesProfile() { .contains("정독함") .contains("깊게 읽음"); - ArgumentCaptor documentCaptor = ArgumentCaptor.forClass(UserProfileDocument.class); - verify(userProfileDocumentRepository).save(documentCaptor.capture()); + ArgumentCaptor documentCaptor = ArgumentCaptor.forClass(PersonalizationProfileDocument.class); + verify(personalizationProfileDocumentRepository).save(documentCaptor.capture()); - UserProfileDocument savedDocument = documentCaptor.getValue(); + PersonalizationProfileDocument savedDocument = documentCaptor.getValue(); assertThat(savedDocument.getUserId()).isEqualTo(userId); assertThat(savedDocument.getProfileText()) .isEqualTo("Java와 Spring 기반 백엔드, Docker 중심 운영 자동화, Elasticsearch 검색 최적화에 집중하는 사용자"); @@ -163,7 +163,7 @@ void generateUserProfileSync_CollectsActivityDataParsesAndSavesProfile() { @Test @DisplayName("LLM 응답을 파싱하지 못하면 전체 텍스트를 프로필로 fallback 저장한다") - void generateUserProfileSync_FallsBackToFullTextWhenSectionsAreMissing() { + void generatePersonalizationProfileSync_FallsBackToFullTextWhenSectionsAreMissing() { Long userId = 2L; User user = createUser(userId); String llmResponse = "섹션 없이도 전체 응답을 개인화 프로필로 저장해야 한다"; @@ -174,16 +174,16 @@ void generateUserProfileSync_FallsBackToFullTextWhenSectionsAreMissing() { given(searchHistoryRepository.findRecentSearchHistoriesByUserId(anyLong(), any())).willReturn(List.of()); given(llmClient.call(anyString(), anyString())).willReturn(llmResponse); given(embeddingClient.embed(llmResponse)).willReturn(List.of(1.0f, 2.0f)); - given(userProfileDocumentRepository.save(any(UserProfileDocument.class))) + given(personalizationProfileDocumentRepository.save(any(PersonalizationProfileDocument.class))) .willAnswer(invocation -> invocation.getArgument(0)); given(userRepository.findById(userId)).willReturn(Optional.of(user)); - userProfileService.generateUserProfileSync(userId); + personalizationProfileService.generatePersonalizationProfileSync(userId); - ArgumentCaptor documentCaptor = ArgumentCaptor.forClass(UserProfileDocument.class); - verify(userProfileDocumentRepository).save(documentCaptor.capture()); + ArgumentCaptor documentCaptor = ArgumentCaptor.forClass(PersonalizationProfileDocument.class); + verify(personalizationProfileDocumentRepository).save(documentCaptor.capture()); - UserProfileDocument savedDocument = documentCaptor.getValue(); + PersonalizationProfileDocument savedDocument = documentCaptor.getValue(); assertThat(savedDocument.getProfileText()).isEqualTo(llmResponse); assertThat(savedDocument.getKeyKeywords()).isEmpty(); assertThat(savedDocument.getProfileVector()).containsExactly(1.0f, 2.0f); @@ -191,7 +191,7 @@ void generateUserProfileSync_FallsBackToFullTextWhenSectionsAreMissing() { @Test @DisplayName("추천 생성이 실패해도 개인화 프로필 저장은 유지된다") - void generateUserProfileSync_RecommendationFailureDoesNotBreakProfileSave() { + void generatePersonalizationProfileSync_RecommendationFailureDoesNotBreakProfileSave() { Long userId = 3L; User user = createUser(userId); @@ -208,16 +208,16 @@ void generateUserProfileSync_RecommendationFailureDoesNotBreakProfileSave() { 테스트, 회귀 """); given(embeddingClient.embed(anyString())).willReturn(List.of(9.0f, 8.0f)); - given(userProfileDocumentRepository.save(any(UserProfileDocument.class))) + given(personalizationProfileDocumentRepository.save(any(PersonalizationProfileDocument.class))) .willAnswer(invocation -> invocation.getArgument(0)); given(userRepository.findById(userId)).willReturn(Optional.of(user)); given(recommendationService.generateRecommendationsForUser(user)) .willThrow(new RuntimeException("recommendation failure")); - assertThatCode(() -> userProfileService.generateUserProfileSync(userId)) + assertThatCode(() -> personalizationProfileService.generatePersonalizationProfileSync(userId)) .doesNotThrowAnyException(); - verify(userProfileDocumentRepository).save(any(UserProfileDocument.class)); + verify(personalizationProfileDocumentRepository).save(any(PersonalizationProfileDocument.class)); verify(recommendationService).generateRecommendationsForUser(user); } diff --git a/src/test/java/com/techfork/evaluation/recommendation/RecommendationEvaluationService.java b/src/test/java/com/techfork/evaluation/recommendation/RecommendationEvaluationService.java index 11506b59..0c38d173 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/RecommendationEvaluationService.java +++ b/src/test/java/com/techfork/evaluation/recommendation/RecommendationEvaluationService.java @@ -17,9 +17,9 @@ import com.techfork.domain.recommendation.service.MmrService.MmrCandidate; import com.techfork.domain.recommendation.service.MmrService.MmrResult; import com.techfork.global.util.RrfScorer; -import com.techfork.domain.user.document.UserProfileDocument; +import com.techfork.domain.user.document.PersonalizationProfileDocument; import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.repository.UserProfileDocumentRepository; +import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; import com.techfork.global.elasticsearch.query.VectorQueryBuilder; import com.techfork.global.util.TimeDecayStrategy; import lombok.extern.slf4j.Slf4j; @@ -39,7 +39,7 @@ @Service public class RecommendationEvaluationService extends LlmRecommendationService { - private final UserProfileDocumentRepository userProfileDocumentRepository; + private final PersonalizationProfileDocumentRepository personalizationProfileDocumentRepository; private final VectorQueryBuilder vectorQueryBuilder; private final ElasticsearchClient elasticsearchClient; @@ -50,7 +50,7 @@ public class RecommendationEvaluationService extends LlmRecommendationService { public RecommendationEvaluationService( ElasticsearchClient elasticsearchClient, - UserProfileDocumentRepository userProfileDocumentRepository, + PersonalizationProfileDocumentRepository personalizationProfileDocumentRepository, RecommendedPostRepository recommendedPostRepository, RecommendationHistoryRepository recommendationHistoryRepository, ReadPostRepository readPostRepository, @@ -60,12 +60,12 @@ public RecommendationEvaluationService( RecommendationProperties properties, VectorQueryBuilder vectorQueryBuilder ) { - super(elasticsearchClient, userProfileDocumentRepository, recommendedPostRepository, + super(elasticsearchClient, personalizationProfileDocumentRepository, recommendedPostRepository, recommendationHistoryRepository, readPostRepository, postRepository, mmrService, timeDecayStrategy, properties, vectorQueryBuilder, Executors.newSingleThreadExecutor()); this.elasticsearchClient = elasticsearchClient; - this.userProfileDocumentRepository = userProfileDocumentRepository; + this.personalizationProfileDocumentRepository = personalizationProfileDocumentRepository; this.vectorQueryBuilder = vectorQueryBuilder; } @@ -75,12 +75,12 @@ public RecommendationEvaluationService( public List generateRecommendationsForEvaluation(User user, Set trainPostIds, RecommendationProperties properties) { long totalStartTime = System.currentTimeMillis(); - Optional profileOpt = userProfileDocumentRepository.findByUserId(user.getId()); + Optional profileOpt = personalizationProfileDocumentRepository.findByUserId(user.getId()); if (profileOpt.isEmpty() || profileOpt.get().getProfileVector() == null) { return Collections.emptyList(); } - UserProfileDocument profile = profileOpt.get(); + PersonalizationProfileDocument profile = profileOpt.get(); float[] userProfileVector = profile.getProfileVector(); List keyKeywords = profile.getKeyKeywords(); @@ -117,12 +117,12 @@ public List generateRecommendationsForEvaluation(User user, Set trai * 1차 후보군만 반환 (MMR bypass) - RRF 결과를 similarityScore 내림차순으로 반환 */ public List generateCandidatesOnly(User user, Set trainPostIds, RecommendationProperties properties) { - Optional profileOpt = userProfileDocumentRepository.findByUserId(user.getId()); + Optional profileOpt = personalizationProfileDocumentRepository.findByUserId(user.getId()); if (profileOpt.isEmpty() || profileOpt.get().getProfileVector() == null) { return Collections.emptyList(); } - UserProfileDocument profile = profileOpt.get(); + PersonalizationProfileDocument profile = profileOpt.get(); float[] userProfileVector = profile.getProfileVector(); List keyKeywords = profile.getKeyKeywords(); diff --git a/src/test/java/com/techfork/evaluation/recommendation/setup/UserDataSetupAndExporter.java b/src/test/java/com/techfork/evaluation/recommendation/setup/UserDataSetupAndExporter.java index 5957c1b9..c18cb223 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/setup/UserDataSetupAndExporter.java +++ b/src/test/java/com/techfork/evaluation/recommendation/setup/UserDataSetupAndExporter.java @@ -7,10 +7,10 @@ import com.techfork.evaluation.recommendation.setup.components.TestDataGenerator; import com.techfork.evaluation.recommendation.setup.components.TestDataGenerator.UserCreationResult; import com.techfork.evaluation.recommendation.util.EvaluationFixtureLoader; -import com.techfork.domain.user.document.UserProfileDocument; +import com.techfork.domain.user.document.PersonalizationProfileDocument; import com.techfork.domain.user.entity.User; import com.techfork.domain.user.enums.EInterestCategory; -import com.techfork.domain.user.repository.UserProfileDocumentRepository; +import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; import com.techfork.domain.user.repository.UserRepository; import com.techfork.global.common.IntegrationTestBase; import lombok.extern.slf4j.Slf4j; @@ -44,7 +44,7 @@ public class UserDataSetupAndExporter extends IntegrationTestBase { private ReadPostRepository readPostRepository; @Autowired - private UserProfileDocumentRepository userProfileDocumentRepository; + private PersonalizationProfileDocumentRepository personalizationProfileDocumentRepository; @Autowired private TestDataGenerator testDataGenerator; @@ -141,7 +141,7 @@ void step2_CreateTestUsers() throws IOException { log.info("총 생성된 사용자: {} 명", users.size()); long profileCount = users.stream() - .filter(u -> userProfileDocumentRepository.findByUserId(u.getId()).isPresent()) + .filter(u -> personalizationProfileDocumentRepository.findByUserId(u.getId()).isPresent()) .count(); log.info("UserProfile(임베딩) 생성된 사용자: {} 명", profileCount); @@ -179,8 +179,8 @@ void step3_ExportUserData() throws IOException { List readPosts = exportReadPosts(users); log.info("✓ 읽은 글 이력 {} 개 export 완료", readPosts.size()); - List userProfiles = exportUserProfiles(users); - log.info("✓ UserProfileDocument {} 개 export 완료 (임베딩 포함)", userProfiles.size()); + List userProfiles = exportUserProfiles(users); + log.info("✓ PersonalizationProfileDocument {} 개 export 완료 (임베딩 포함)", userProfiles.size()); log.info("===== STEP 3 완료 ====="); log.info("출력 위치: {}", fileExporter.getOutputDir()); @@ -225,24 +225,24 @@ private List exportReadPosts(List users) throws IOException { return allReadPosts; } - private List exportUserProfiles(List users) throws IOException { - List profiles = new ArrayList<>(); + private List exportUserProfiles(List users) throws IOException { + List profiles = new ArrayList<>(); int notFoundCount = 0; for (User user : users) { - Optional profileOpt = - userProfileDocumentRepository.findByUserId(user.getId()); + Optional profileOpt = + personalizationProfileDocumentRepository.findByUserId(user.getId()); if (profileOpt.isPresent()) { profiles.add(profileOpt.get()); } else { notFoundCount++; - log.warn("UserProfileDocument not found for userId: {}", user.getId()); + log.warn("PersonalizationProfileDocument not found for userId: {}", user.getId()); } } if (notFoundCount > 0) { - log.warn("총 {} 명의 UserProfileDocument를 찾지 못했습니다.", notFoundCount); + log.warn("총 {} 명의 PersonalizationProfileDocument를 찾지 못했습니다.", notFoundCount); } // DTO 변환 (profileVector는 float[]이므로 List로 변환) @@ -254,7 +254,7 @@ private List exportUserProfiles(List users) throws IO // 임베딩 차원 검증 if (!profiles.isEmpty()) { - UserProfileDocument sample = profiles.get(0); + PersonalizationProfileDocument sample = profiles.get(0); log.info("임베딩 차원 검증:"); log.info(" - profileVector: {} 차원", sample.getProfileVector() != null ? sample.getProfileVector().length : "null"); @@ -311,7 +311,7 @@ private Map convertReadPostToDto(ReadPost readPost) { return dto; } - private Map convertUserProfileToDto(UserProfileDocument profile) { + private Map convertUserProfileToDto(PersonalizationProfileDocument profile) { Map dto = new HashMap<>(); dto.put("id", profile.getId()); dto.put("userId", profile.getUserId()); diff --git a/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthGenerator.java b/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthGenerator.java index fe2af6e4..11b83428 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthGenerator.java +++ b/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthGenerator.java @@ -3,7 +3,7 @@ import com.techfork.domain.post.document.PostDocument; import com.techfork.domain.post.entity.Post; import com.techfork.domain.post.repository.PostDocumentRepository; -import com.techfork.domain.user.document.UserProfileDocument; +import com.techfork.domain.user.document.PersonalizationProfileDocument; import com.techfork.global.llm.LlmClient; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -37,7 +37,7 @@ public class GroundTruthGenerator { */ public Map calculateGroundTruth( List posts, - UserProfileDocument userProfile) { + PersonalizationProfileDocument userProfile) { Map groundTruthScores = new HashMap<>(); int count = 0; @@ -62,7 +62,7 @@ public Map calculateGroundTruth( /** * LLM을 사용하여 게시글의 관련도 점수 평가 (1~5점) */ - private int calculateRelevanceScoreWithLLM(Post post, UserProfileDocument userProfile) { + private int calculateRelevanceScoreWithLLM(Post post, PersonalizationProfileDocument userProfile) { // PostDocument에서 더 풍부한 정보 가져오기 (요약문, 본문 청크 등) Optional postDocOpt = postDocumentRepository.findByPostId(post.getId()); diff --git a/src/test/java/com/techfork/evaluation/recommendation/setup/components/TestDataGenerator.java b/src/test/java/com/techfork/evaluation/recommendation/setup/components/TestDataGenerator.java index 3d519b5a..d546ea80 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/setup/components/TestDataGenerator.java +++ b/src/test/java/com/techfork/evaluation/recommendation/setup/components/TestDataGenerator.java @@ -2,11 +2,11 @@ import com.techfork.domain.post.entity.Post; import com.techfork.domain.post.repository.PostRepository; -import com.techfork.domain.user.document.UserProfileDocument; +import com.techfork.domain.user.document.PersonalizationProfileDocument; import com.techfork.domain.user.entity.User; import com.techfork.domain.user.enums.EInterestCategory; -import com.techfork.domain.user.repository.UserProfileDocumentRepository; -import com.techfork.domain.user.service.UserProfileService; +import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; +import com.techfork.domain.user.service.PersonalizationProfileService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; @@ -33,8 +33,8 @@ public record UserCreationResult( ) {} private final PostRepository postRepository; - private final UserProfileService userProfileService; - private final UserProfileDocumentRepository userProfileDocumentRepository; + private final PersonalizationProfileService personalizationProfileService; + private final PersonalizationProfileDocumentRepository personalizationProfileDocumentRepository; private final org.springframework.data.elasticsearch.core.ElasticsearchOperations elasticsearchOperations; // 분리된 컴포넌트 @@ -123,14 +123,14 @@ public UserCreationResult createTestUserWithGroundTruth( // UserProfile 생성 (임베딩 포함) - 동기 버전 사용 // Ground Truth 점수 계산 전에 프로필 벡터가 필요함 - UserProfileDocument userProfile = null; + PersonalizationProfileDocument userProfile = null; try { - userProfileService.generateUserProfileSync(user.getId()); + personalizationProfileService.generatePersonalizationProfileSync(user.getId()); // Elasticsearch Refresh: 저장이 검색 가능해지도록 강제 갱신 - elasticsearchOperations.indexOps(UserProfileDocument.class).refresh(); + elasticsearchOperations.indexOps(PersonalizationProfileDocument.class).refresh(); - Optional userProfileOpt = userProfileDocumentRepository.findByUserId(user.getId()); + Optional userProfileOpt = personalizationProfileDocumentRepository.findByUserId(user.getId()); if (userProfileOpt.isPresent()) { userProfile = userProfileOpt.get(); log.info("사용자 프로필 및 임베딩 생성 완료: userId={}", user.getId()); diff --git a/src/test/java/com/techfork/evaluation/recommendation/util/EvaluationFixtureLoader.java b/src/test/java/com/techfork/evaluation/recommendation/util/EvaluationFixtureLoader.java index 5d92befb..ad75315e 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/util/EvaluationFixtureLoader.java +++ b/src/test/java/com/techfork/evaluation/recommendation/util/EvaluationFixtureLoader.java @@ -12,7 +12,7 @@ import com.techfork.domain.post.repository.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; -import com.techfork.domain.user.document.UserProfileDocument; +import com.techfork.domain.user.document.PersonalizationProfileDocument; import com.techfork.domain.user.entity.User; import com.techfork.domain.user.entity.UserInterestCategory; import com.techfork.domain.user.entity.UserInterestKeyword; @@ -20,7 +20,7 @@ import com.techfork.domain.user.enums.EInterestKeyword; import com.techfork.domain.user.enums.Role; import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserProfileDocumentRepository; +import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; import com.techfork.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -47,7 +47,7 @@ public class EvaluationFixtureLoader { private final PostRepository postRepository; private final ReadPostRepository readPostRepository; private final PostDocumentRepository postDocumentRepository; - private final UserProfileDocumentRepository userProfileDocumentRepository; + private final PersonalizationProfileDocumentRepository personalizationProfileDocumentRepository; private final TechBlogRepository techBlogRepository; private final ObjectMapper objectMapper = new ObjectMapper() @@ -73,7 +73,7 @@ public Map> loadAll() { log.info("✓ PostDocument {} 개 로드 완료 (임베딩 포함)", postDocCount); int userProfileCount = loadUserProfiles(userMap); - log.info("✓ UserProfileDocument {} 개 로드 완료 (임베딩 포함)", userProfileCount); + log.info("✓ PersonalizationProfileDocument {} 개 로드 완료 (임베딩 포함)", userProfileCount); Map> groundTruth = loadGroundTruth(userMap, postMap); log.info("✓ Ground Truth {} 명 사용자 로드 완료", groundTruth.size()); @@ -355,7 +355,7 @@ private int loadUserProfiles(Map userMap) throws IOException { } } - UserProfileDocument profile = UserProfileDocument.builder() + PersonalizationProfileDocument profile = PersonalizationProfileDocument.builder() .userId(actualUserId) .profileText(profileText) .profileVector(profileVector) @@ -363,7 +363,7 @@ private int loadUserProfiles(Map userMap) throws IOException { .keyKeywords(keyKeywords) .build(); - userProfileDocumentRepository.save(profile); + personalizationProfileDocumentRepository.save(profile); count++; } diff --git a/src/test/java/com/techfork/evaluation/search/SearchEvaluationTestBase.java b/src/test/java/com/techfork/evaluation/search/SearchEvaluationTestBase.java index 6d670a68..ea7971b1 100644 --- a/src/test/java/com/techfork/evaluation/search/SearchEvaluationTestBase.java +++ b/src/test/java/com/techfork/evaluation/search/SearchEvaluationTestBase.java @@ -11,7 +11,7 @@ import com.techfork.domain.search.config.GeneralSearchProperties; import com.techfork.domain.search.service.SearchService; import com.techfork.domain.search.service.SearchServiceImpl; -import com.techfork.domain.user.repository.UserProfileDocumentRepository; +import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; import com.techfork.evaluation.search.util.GroundTruthItem; import com.techfork.evaluation.search.util.SearchQualityService; import com.techfork.global.config.CloudflareThirdPartyThumbnailOptimizationProperties; @@ -48,7 +48,7 @@ public abstract class SearchEvaluationTestBase { @Autowired protected ElasticsearchClient elasticsearchClient; @Autowired protected EmbeddingClient embeddingClient; - @Autowired protected UserProfileDocumentRepository userProfileDocumentRepository; + @Autowired protected PersonalizationProfileDocumentRepository personalizationProfileDocumentRepository; @Autowired protected PostRepository postRepository; @Autowired protected BookmarkRepository bookmarkRepository; @Autowired @Qualifier("searchAsyncExecutor") protected Executor searchAsyncExecutor; @@ -109,7 +109,7 @@ protected Map runEvaluation( SearchService svc = new SearchServiceImpl( elasticsearchClient, embeddingClient, props, - userProfileDocumentRepository, postRepository, + personalizationProfileDocumentRepository, postRepository, bookmarkRepository, searchAsyncExecutor, thumbnailOptimizer); // index: [nDCG@4, nDCG@8, nDCG@20, Recall@4, Recall@8, Recall@20, latency] diff --git a/src/test/java/com/techfork/evaluation/search/setup/UserProfileServiceTest.java b/src/test/java/com/techfork/evaluation/search/setup/PersonalizationProfileServiceTest.java similarity index 91% rename from src/test/java/com/techfork/evaluation/search/setup/UserProfileServiceTest.java rename to src/test/java/com/techfork/evaluation/search/setup/PersonalizationProfileServiceTest.java index 3003509d..8afc5559 100644 --- a/src/test/java/com/techfork/evaluation/search/setup/UserProfileServiceTest.java +++ b/src/test/java/com/techfork/evaluation/search/setup/PersonalizationProfileServiceTest.java @@ -6,7 +6,7 @@ import com.techfork.domain.user.enums.SocialType; import com.techfork.domain.user.repository.UserInterestCategoryRepository; import com.techfork.domain.user.repository.UserRepository; -import com.techfork.domain.user.service.UserProfileService; +import com.techfork.domain.user.service.PersonalizationProfileService; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; @@ -25,7 +25,7 @@ @Tag("evaluation-setup") @Disabled("데이터 셋업용 - CI 제외") @SpringBootTest -class UserProfileServiceTest { +class PersonalizationProfileServiceTest { @Autowired private UserRepository userRepository; @@ -34,7 +34,7 @@ class UserProfileServiceTest { private UserInterestCategoryRepository userInterestCategoryRepository; @Autowired - private UserProfileService userProfileService; + private PersonalizationProfileService personalizationProfileService; @Test @DisplayName("10명의 각기 다른 관심사를 가진 테스트 사용자를 생성하고 프로필 벡터를 생성한다.") @@ -68,7 +68,7 @@ void generateTestUserProfiles() { userInterestCategoryRepository.saveAll(interestCategories); System.out.println("Generating profile for user: " + user.getId()); - userProfileService.generateUserProfile(user.getId()); + personalizationProfileService.generatePersonalizationProfile(user.getId()); System.out.println("Profile generated for user: " + user.getId()); }); } diff --git a/src/test/java/com/techfork/evaluation/search/setup/SearchGroundTruthGenerator.java b/src/test/java/com/techfork/evaluation/search/setup/SearchGroundTruthGenerator.java index a8f517dd..dffe6371 100644 --- a/src/test/java/com/techfork/evaluation/search/setup/SearchGroundTruthGenerator.java +++ b/src/test/java/com/techfork/evaluation/search/setup/SearchGroundTruthGenerator.java @@ -8,7 +8,7 @@ import com.techfork.domain.search.dto.SearchResult; import com.techfork.domain.search.config.GeneralSearchProperties; import com.techfork.domain.search.service.SearchServiceImpl; -import com.techfork.domain.user.repository.UserProfileDocumentRepository; +import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; import com.techfork.evaluation.recommendation.setup.components.FileExporter; import com.techfork.evaluation.search.util.GroundTruthItem; import com.techfork.global.config.CloudflareThirdPartyThumbnailOptimizationProperties; @@ -102,7 +102,7 @@ class SearchGroundTruthGenerator { private GeneralSearchProperties generalSearchProperties; @Autowired - private UserProfileDocumentRepository userProfileDocumentRepository; + private PersonalizationProfileDocumentRepository personalizationProfileDocumentRepository; @Autowired private PostRepository postRepository; @@ -145,7 +145,7 @@ void generateSearchGroundTruth() throws IOException { ); SearchServiceImpl searchService = new SearchServiceImpl( elasticsearchClient, embeddingClient, generalSearchProperties, - userProfileDocumentRepository, postRepository, bookmarkRepository, searchAsyncExecutor, thumbnailOptimizer); + personalizationProfileDocumentRepository, postRepository, bookmarkRepository, searchAsyncExecutor, thumbnailOptimizer); List groundTruthItems = scoreAllQueries(uniqueQueryMap, searchService); log.info("최종 ground-truth 항목 수: {}", groundTruthItems.size()); From 9b979cc20dafd0e24381e364c0c8c12165bde8cd Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Tue, 28 Apr 2026 21:47:55 +0900 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20AccountProfile=EA=B3=BC=20Perso?= =?UTF-8?q?nalizationProfile=EC=9D=84=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ddd-test-refactoring-roadmap.md | 46 +-- docs/domain-strategy.md | 28 +- docs/test-gap-analysis.md | 38 +-- docs/ubiquitous-language/README.md | 4 +- .../personalization-profile.md | 18 +- docs/ubiquitous-language/search.md | 2 +- .../domain/activity/entity/Bookmark.java | 104 +++--- .../domain/activity/entity/ReadPost.java | 102 +++--- .../domain/activity/entity/SearchHistory.java | 46 +-- .../repository/BookmarkRepository.java | 98 +++--- .../repository/ReadPostRepository.java | 100 +++--- .../service/ActivityCommandService.java | 204 +++++------ .../service/ActivityQueryService.java | 316 +++++++++--------- .../domain/auth/converter/AuthConverter.java | 2 +- .../domain/auth/service/AuthService.java | 8 +- .../entity/NotificationToken.java | 48 +-- .../PersonalizationProfileDocument.java | 2 +- ...sonalizationProfileDocumentRepository.java | 4 +- .../PersonalizationProfileScheduler.java | 8 +- .../PersonalizationProfileService.java | 16 +- .../entity/RecommendationHistory.java | 184 +++++----- .../entity/RecommendedPost.java | 144 ++++---- .../repository/RecommendedPostRepository.java | 52 +-- .../scheduler/RecommendationScheduler.java | 122 +++---- .../service/LlmRecommendationService.java | 6 +- .../service/RecommendationCommandService.java | 48 +-- .../service/RecommendationQueryService.java | 118 +++---- .../service/RecommendationService.java | 36 +- .../search/service/SearchServiceImpl.java | 4 +- .../controller/OnboardingController.java | 102 +++--- .../controller/UserController.java | 130 +++---- .../converter/InterestConverter.java | 94 +++--- .../converter/UserConverter.java | 6 +- .../dto/AccountProfileResponse.java | 2 +- .../dto/InterestListResponse.java | 44 +-- .../dto/OnboardingRequest.java | 2 +- .../dto/SaveInterestRequest.java | 26 +- .../dto/UpdateAccountProfileRequest.java | 2 +- .../dto/UserInterestDto.java | 24 +- .../dto/UserInterestResponse.java | 22 +- .../{user => useraccount}/entity/User.java | 222 ++++++------ .../entity/UserInterestCategory.java | 100 +++--- .../entity/UserInterestKeyword.java | 86 ++--- .../enums/EInterestCategory.java | 88 ++--- .../enums/EInterestKeyword.java | 304 ++++++++--------- .../{user => useraccount}/enums/Role.java | 2 +- .../enums/SocialType.java | 2 +- .../enums/UserStatus.java | 2 +- .../exception/UserErrorCode.java | 52 +-- .../UserInterestCategoryRepository.java | 36 +- .../repository/UserRepository.java | 104 +++--- .../service/InterestCommandService.java | 21 +- .../service/InterestQueryService.java | 94 +++--- .../service/UserCommandService.java | 16 +- .../service/UserQueryService.java | 12 +- .../config/ElasticsearchCacheManager.java | 2 +- .../auth/service/UserAuthCacheService.java | 6 +- .../filter/JwtAuthenticationFilter.java | 6 +- .../OAuth2AuthenticationSuccessHandler.java | 2 +- .../techfork/global/security/jwt/JwtUtil.java | 2 +- .../security/oauth/CustomOidcUserService.java | 6 +- .../global/security/oauth/UserPrincipal.java | 6 +- .../ActivityControllerIntegrationTest.java | 8 +- .../repository/BookmarkRepositoryTest.java | 6 +- .../repository/ReadPostRepositoryTest.java | 6 +- .../SearchHistoryRepositoryTest.java | 6 +- .../service/ActivityCommandServiceTest.java | 6 +- .../service/ActivityQueryServiceTest.java | 6 +- .../AdminControllerIntegrationTest.java | 8 +- .../AuthControllerIntegrationTest.java | 10 +- .../domain/auth/service/AuthServiceTest.java | 10 +- .../PersonalizationProfileServiceTest.java | 22 +- .../PostControllerIntegrationTest.java | 8 +- .../PostControllerV2IntegrationTest.java | 8 +- ...commendationControllerIntegrationTest.java | 8 +- .../RecommendationConverterTest.java | 4 +- .../RecommendationQueryServiceTest.java | 6 +- .../OnboardingControllerIntegrationTest.java | 14 +- .../UserControllerIntegrationTest.java | 14 +- .../UserInterestCategoryRepositoryTest.java | 16 +- .../repository/UserRepositoryTest.java | 10 +- .../service/InterestCommandServiceTest.java | 19 +- .../service/UserCommandServiceTest.java | 22 +- .../service/UserQueryServiceTest.java | 16 +- .../recommendation/KValueComparisonTest.java | 2 +- .../LambdaOptimizationTest.java | 2 +- .../MmrCandidateSizeComparisonTest.java | 2 +- .../RecommendationConfigComparisonTest.java | 2 +- .../RecommendationEvaluationService.java | 6 +- .../RecommendationTestBase.java | 4 +- .../TitleSummaryRatioOptimizationTest.java | 2 +- .../setup/UserDataSetupAndExporter.java | 10 +- .../components/GroundTruthGenerator.java | 2 +- .../components/GroundTruthValidator.java | 2 +- .../setup/components/PostMatcher.java | 2 +- .../setup/components/TestDataGenerator.java | 10 +- .../setup/components/UserTestDataBuilder.java | 16 +- .../util/EvaluationFixtureLoader.java | 20 +- .../search/SearchEvaluationTestBase.java | 2 +- .../PersonalizationProfileServiceTest.java | 16 +- .../setup/SearchGroundTruthGenerator.java | 2 +- .../security/SecurityIntegrationTest.java | 8 +- .../service/UserAuthCacheServiceTest.java | 8 +- .../filter/JwtAuthenticationFilterTest.java | 10 +- 104 files changed, 1948 insertions(+), 1946 deletions(-) rename src/main/java/com/techfork/domain/{user => personalization}/document/PersonalizationProfileDocument.java (97%) rename src/main/java/com/techfork/domain/{user => personalization}/repository/PersonalizationProfileDocumentRepository.java (69%) rename src/main/java/com/techfork/domain/{user => personalization}/scheduler/PersonalizationProfileScheduler.java (85%) rename src/main/java/com/techfork/domain/{user => personalization}/service/PersonalizationProfileService.java (95%) rename src/main/java/com/techfork/domain/{user => useraccount}/controller/OnboardingController.java (85%) rename src/main/java/com/techfork/domain/{user => useraccount}/controller/UserController.java (87%) rename src/main/java/com/techfork/domain/{user => useraccount}/converter/InterestConverter.java (80%) rename src/main/java/com/techfork/domain/{user => useraccount}/converter/UserConverter.java (72%) rename src/main/java/com/techfork/domain/{user => useraccount}/dto/AccountProfileResponse.java (80%) rename src/main/java/com/techfork/domain/{user => useraccount}/dto/InterestListResponse.java (86%) rename src/main/java/com/techfork/domain/{user => useraccount}/dto/OnboardingRequest.java (94%) rename src/main/java/com/techfork/domain/{user => useraccount}/dto/SaveInterestRequest.java (87%) rename src/main/java/com/techfork/domain/{user => useraccount}/dto/UpdateAccountProfileRequest.java (89%) rename src/main/java/com/techfork/domain/{user => useraccount}/dto/UserInterestDto.java (75%) rename src/main/java/com/techfork/domain/{user => useraccount}/dto/UserInterestResponse.java (74%) rename src/main/java/com/techfork/domain/{user => useraccount}/entity/User.java (91%) rename src/main/java/com/techfork/domain/{user => useraccount}/entity/UserInterestCategory.java (90%) rename src/main/java/com/techfork/domain/{user => useraccount}/entity/UserInterestKeyword.java (90%) rename src/main/java/com/techfork/domain/{user => useraccount}/enums/EInterestCategory.java (89%) rename src/main/java/com/techfork/domain/{user => useraccount}/enums/EInterestKeyword.java (96%) rename src/main/java/com/techfork/domain/{user => useraccount}/enums/Role.java (80%) rename src/main/java/com/techfork/domain/{user => useraccount}/enums/SocialType.java (91%) rename src/main/java/com/techfork/domain/{user => useraccount}/enums/UserStatus.java (87%) rename src/main/java/com/techfork/domain/{user => useraccount}/exception/UserErrorCode.java (92%) rename src/main/java/com/techfork/domain/{user => useraccount}/repository/UserInterestCategoryRepository.java (81%) rename src/main/java/com/techfork/domain/{user => useraccount}/repository/UserRepository.java (90%) rename src/main/java/com/techfork/domain/{user => useraccount}/service/InterestCommandService.java (79%) rename src/main/java/com/techfork/domain/{user => useraccount}/service/InterestQueryService.java (67%) rename src/main/java/com/techfork/domain/{user => useraccount}/service/UserCommandService.java (83%) rename src/main/java/com/techfork/domain/{user => useraccount}/service/UserQueryService.java (68%) rename src/test/java/com/techfork/domain/{user => personalization}/service/PersonalizationProfileServiceTest.java (94%) rename src/test/java/com/techfork/domain/{user => useraccount}/controller/OnboardingControllerIntegrationTest.java (96%) rename src/test/java/com/techfork/domain/{user => useraccount}/controller/UserControllerIntegrationTest.java (95%) rename src/test/java/com/techfork/domain/{user => useraccount}/repository/UserInterestCategoryRepositoryTest.java (95%) rename src/test/java/com/techfork/domain/{user => useraccount}/repository/UserRepositoryTest.java (97%) rename src/test/java/com/techfork/domain/{user => useraccount}/service/InterestCommandServiceTest.java (93%) rename src/test/java/com/techfork/domain/{user => useraccount}/service/UserCommandServiceTest.java (95%) rename src/test/java/com/techfork/domain/{user => useraccount}/service/UserQueryServiceTest.java (87%) diff --git a/docs/ddd-test-refactoring-roadmap.md b/docs/ddd-test-refactoring-roadmap.md index 53e79c53..503be85e 100644 --- a/docs/ddd-test-refactoring-roadmap.md +++ b/docs/ddd-test-refactoring-roadmap.md @@ -47,7 +47,7 @@ DDD 목표 지도 작성 - 예: `ScrabPost`, `scrap_posts`, `Bookmark` - 예: `searchWord`, `query`, `keyKeywords`, `PostKeyword` - 예: 계정 프로필과 개인화 프로필 -- Search, Recommendation, Personalization Profile(`UserProfileService`) 쪽은 여러 컨텍스트와 외부 인프라가 얽혀 있어 리팩터링 위험이 크다. +- Search, Recommendation, Personalization Profile(`PersonalizationProfileService`) 쪽은 여러 컨텍스트와 외부 인프라가 얽혀 있어 리팩터링 위험이 크다. 따라서 안전한 전환 전략은 다음이다. @@ -265,14 +265,14 @@ PostKeyword | 표준 용어 | 코드상 표현 | 의미 | |---|---|---| | 계정 프로필 | `User.nickName`, `description`, `profileImage` | 사용자에게 보이는 기본 프로필 | -| 개인화 프로필 | `UserProfileDocument.profileText`, `profileVector` | 검색/추천에 쓰이는 활동 기반 LLM/임베딩 프로필 | +| 개인화 프로필 | `PersonalizationProfileDocument.profileText`, `profileVector` | 검색/추천에 쓰이는 활동 기반 LLM/임베딩 프로필 | 권장 순서: ```text 1. 문서/API 설명에서 `User Account`와 `Personalization Profile` 경계를 고정 -2. UserProfileService 테스트 작성 -3. UserProfileDocument의 역할을 Personalization Profile projection으로 명확히 함 +2. PersonalizationProfileService 테스트 작성 +3. PersonalizationProfileDocument의 역할을 Personalization Profile projection으로 명확히 함 4. 필요하면 패키지/이벤트/포트 분리를 후속 단계에서 진행 ``` @@ -458,14 +458,14 @@ InterestCommandServiceTest ```text Personalization Profile -- UserProfileDocument (개인화 검색/추천용 read model projection) -- UserProfileService (Personalization Profile 생성 서비스로 위치 재정의) +- PersonalizationProfileDocument (개인화 검색/추천용 read model projection) +- PersonalizationProfileService (Personalization Profile 생성 서비스로 위치 재정의) ``` ##### 먼저 작성할 테스트 ```text -UserProfileServiceTest +PersonalizationProfileServiceTest - 관심사, 읽은 게시글, 북마크, 검색 기록을 모아 활동 데이터를 구성한다. - LLM 응답에서 프로필 텍스트와 핵심 키워드를 파싱한다. - 파싱 실패 시 fallback 정책을 따른다. @@ -479,7 +479,7 @@ UserProfileServiceTest 현재 Personalization Profile 생성 서비스 의존: ```text -UserProfileService +PersonalizationProfileService - User 관심사 - ReadPost - Bookmark @@ -492,8 +492,8 @@ UserProfileService 정리 방향: -- `UserProfileDocument`를 Personalization Profile projection으로 명확히 한다. -- `UserProfileService`를 User Account 서비스가 아닌 Personalization Profile 생성 서비스로 위치를 재정의한다. +- `PersonalizationProfileDocument`를 Personalization Profile projection으로 명확히 한다. +- `PersonalizationProfileService`를 User Account 서비스가 아닌 Personalization Profile 생성 서비스로 위치를 재정의한다. - 관심사 변경/온보딩 완료는 장기적으로 `UserInterestsChanged`, `OnboardingCompleted` 이벤트로 분리한다. 분리 후보 (점진적으로 적용): @@ -514,7 +514,7 @@ PersonalizedProfileRepository ##### 왜 네 번째인가 - 복잡도가 높다. -- Elasticsearch, Personalization Profile(`UserProfileDocument`), Activity, Post에 모두 의존한다. +- Elasticsearch, Personalization Profile(`PersonalizationProfileDocument`), Activity, Post에 모두 의존한다. - 테스트 없이 건드리면 위험하다. ##### 목표 모델 @@ -587,7 +587,7 @@ SearchServiceImplTest - Elasticsearch 호출을 adapter로 감싸기 - `PostDocument`를 검색 read model로 명시 -- `UserProfileDocument`를 Personalization Profile read model로 명시 +- `PersonalizationProfileDocument`를 Personalization Profile read model로 명시 - Activity의 북마크 여부 조회를 query composition으로 유지하되 포트 도입 검토 --- @@ -651,7 +651,7 @@ src/test/java/com/techfork/domain user UserTest InterestCommandServiceTest - UserProfileServiceTest + PersonalizationProfileServiceTest recommendation MmrServiceTest @@ -686,7 +686,7 @@ src/test/java/com/techfork/domain ```text [ ] P0 테스트가 모두 존재하고 ./gradlew test -PexcludeIntegration 통과 -[ ] UserProfileServiceTest로 Personalization Profile 생성 흐름 보호 +[ ] PersonalizationProfileServiceTest로 Personalization Profile 생성 흐름 보호 [ ] MmrServiceTest + LlmRecommendationServiceTest로 추천 생성 핵심 흐름 보호 [ ] SearchServiceImplTest로 일반/개인화 검색 회귀 보호 [ ] User Account aggregate 책임과 Personalization Profile 생성 책임이 @@ -737,7 +737,7 @@ TechnicalPostIndexed 4. Post 도메인 테스트 작성 5. Post를 “기술 게시글” 기준으로 정리 6. User 관심사/온보딩 테스트 작성 -7. UserProfileService 테스트 작성 +7. PersonalizationProfileService 테스트 작성 8. Personalization Profile 책임 분리 9. Recommendation 테스트 작성 10. PersonalizedProfileGenerated 이벤트 도입 @@ -802,8 +802,8 @@ TechnicalPostIndexed - 핵심 키워드 파싱 리팩터링: -- UserProfileDocument를 Personalization Profile projection으로 명확히 함 -- UserProfileService 책임 분리 +- PersonalizationProfileDocument를 Personalization Profile projection으로 명확히 함 +- PersonalizationProfileService 책임 분리 - PersonalizedProfileGenerated 이벤트 도입 준비 ``` @@ -866,19 +866,19 @@ TechnicalPostIndexed ```text 목표: -- UserProfileService를 Personalization Profile 생성 서비스로 테스트로 고정한다. +- PersonalizationProfileService를 Personalization Profile 생성 서비스로 테스트로 고정한다. 선행 테스트: - 관심사, 읽은 게시글, 북마크, 검색 기록을 활동 데이터로 수집한다. - LLM 응답에서 profileText와 keyKeywords를 파싱한다. - 파싱 실패 시 fallback 정책을 따른다. -- profileText를 임베딩하여 UserProfileDocument를 저장한다. +- profileText를 임베딩하여 PersonalizationProfileDocument를 저장한다. - 개인화 프로필 생성 후 추천 생성을 호출한다. - 추천 생성 실패가 개인화 프로필 저장을 깨뜨리지 않는다. 리팩터링: -- UserProfileDocument를 Personalization Profile projection으로 명확히 함 -- UserProfileService 책임 분리 (PersonalizedProfileGenerated 이벤트 도입 준비) +- PersonalizationProfileDocument를 Personalization Profile projection으로 명확히 함 +- PersonalizationProfileService 책임 분리 (PersonalizedProfileGenerated 이벤트 도입 준비) ``` ### 5.8 작업 단위 8: 1차 이벤트 도입 @@ -893,11 +893,11 @@ TechnicalPostIndexed 도입 순서: 1. UserInterestsChanged - - InterestCommandService가 UserProfileService를 직접 호출하는 대신 이벤트 발행 + - InterestCommandService가 PersonalizationProfileService를 직접 호출하는 대신 이벤트 발행 - 리스너: @TransactionalEventListener(AFTER_COMMIT) + @Async 2. PersonalizedProfileGenerated - - UserProfileService가 LlmRecommendationService를 직접 호출하는 대신 이벤트 발행 + - PersonalizationProfileService가 LlmRecommendationService를 직접 호출하는 대신 이벤트 발행 - 리스너: 추천 생성 트리거 3. TechnicalPostIndexed diff --git a/docs/domain-strategy.md b/docs/domain-strategy.md index d7a9486a..5acee8cf 100644 --- a/docs/domain-strategy.md +++ b/docs/domain-strategy.md @@ -52,11 +52,11 @@ TechFork의 비즈니스 도메인은 다음으로 정의할 수 있다. - `SearchServiceImpl` - `LlmRecommendationService` - `MmrService` -- `UserProfileService` +- `PersonalizationProfileService` - `SummaryExtractionService` - `ContentChunkerService` - `PostEmbeddingProcessor` -- `PostDocument`, `UserProfileDocument` +- `PostDocument`, `PersonalizationProfileDocument` #### 지원 하위 도메인 Supporting Subdomains @@ -87,7 +87,7 @@ TechFork의 비즈니스 도메인은 다음으로 정의할 수 있다. - 다만 Auth / Security, Activity, Notification이 기대는 사용자 정체성 경계를 제공한다. - `Personalization Profile` 컨텍스트는 핵심 하위 도메인에 가깝다. - 개인화 프로필 생성, 프로필 벡터, 핵심 키워드, 재생성 정책은 검색/추천 품질의 중심이다. - - 현재 구현에서는 `UserProfileDocument`가 독립 aggregate보다 read model/projection 성격이 강하고, 생성 책임도 `domain/user` 아래 서비스에 함께 묶여 있다. + - 현재 구현에서는 `PersonalizationProfileDocument`가 독립 aggregate보다 read model/projection 성격이 강하고, 생성 책임도 `domain/user` 아래 서비스에 함께 묶여 있다. - `Post / Content` 컨텍스트 전체가 핵심은 아니다. - 단순 목록/상세 조회는 지원 하위 도메인이다. - 요약, 키워드 추출, 청크, 임베딩, 검색 문서화는 핵심 하위 도메인에 가깝다. @@ -124,15 +124,15 @@ TechFork의 비즈니스 도메인은 다음으로 정의할 수 있다. 의미: 1. `User` aggregate는 당분간 `User Account` 컨텍스트의 핵심 루트로 본다. -2. `UserProfileDocument`는 `Personalization Profile` 컨텍스트의 핵심 projection/read model로 본다. +2. `PersonalizationProfileDocument`는 `Personalization Profile` 컨텍스트의 핵심 projection/read model로 본다. 3. Search/Recommendation과의 관계 해석은 `User Account`와 `Personalization Profile`을 분리해서 본다. 4. 패키지 분리, 포트 분리, 이벤트 분리는 후속 리팩터링 과제로 남긴다. 현재 패키지를 유지하는 이유: 1. `domain/user` 내부에서 계정/온보딩/관심사/개인화 프로필 생성이 아직 함께 구현되어 있다. -2. `UserProfileService`는 Activity/Post/Recommendation을 조합하는 애플리케이션 서비스 성격이 강하지만, 이를 뒷받침하는 독립 패키지/포트/이벤트 경계는 아직 없다. -3. `UserProfileDocument`는 독립 write aggregate보다 검색·추천용 read model에 가깝다. +2. `PersonalizationProfileService`는 Activity/Post/Recommendation을 조합하는 애플리케이션 서비스 성격이 강하지만, 이를 뒷받침하는 독립 패키지/포트/이벤트 경계는 아직 없다. +3. `PersonalizationProfileDocument`는 독립 write aggregate보다 검색·추천용 read model에 가깝다. 향후 아래 조건이 충족되면 실제 패키지/구현도 둘로 나누는 것을 다시 검토한다. @@ -216,20 +216,20 @@ U/D 표기: **U** = Upstream(상류, 공급자), **D** = Downstream(하류, 소 | Source / Ingestion ↔ Post / Content | 출처 식별/표시 | `Post / Content` | `Source / Ingestion` | **직접 엔티티 참조** (약한 SK처럼 보이는 구현 결합) | **OHS/PL** | source reference / 출처 스냅샷 소비 | `Post.techBlog`가 `TechBlog` 엔티티를 직접 참조 | 지금은 direct entity reference라서 SK처럼 보이지만, 목표 상태는 `sourceId`나 출처 스냅샷 소비에 가깝다. | | Activity ↔ User Account | 행동 주체 식별 | `Activity` | `User Account` | **직접 엔티티 참조** (의도상 최소 SK) | **SK** (최소 Shared Identity) | 사용자 귀속 참조 | `ReadPost`, `Bookmark`, `SearchHistory`가 `User`를 직접 참조 | Activity는 사용자 정체성만 알면 된다. 장기적으로는 `UserId` 중심 참조가 더 자연스럽다. | | Activity ↔ Post / Content | 행동 대상 식별 | `Activity` | `Post / Content` | **CF** | **CF** (Conformist) | 직접 aggregate 참조 | `ReadPost`, `Bookmark`가 `Post`를 직접 참조 | Activity는 행동 대상인 기술 게시글 모델을 따른다. 장기적으로는 `PostId` 중심 참조 축소도 가능하다. | -| User Account ↔ Personalization Profile | 프로필 생성 트리거 | `Personalization Profile` | `User Account` | **동기 직접 호출** | **OHS/PL** (Open Host Service / Published Language) | 도메인 이벤트 / Published Language | 현재는 `UserCommandService`, `InterestCommandService`가 `UserProfileService`를 직접 호출 | 목표 상태는 `OnboardingCompleted`, `UserInterestsChanged` 같은 이벤트 handoff다. | -| Personalization Profile ↔ Activity | 행동 요약 입력 | `Personalization Profile` | `Activity` | **CS** | **OHS/PL** | 조회 포트 / `UserActivitySummary` 같은 Published Language | `UserProfileService`가 Activity repository를 직접 조회 | 이 seam은 현재는 Customer-Supplier에 가깝지만, 목표 상태는 “행동 요약 언어를 소비한다”는 OHS/PL이 더 자연스럽다. | -| Personalization Profile ↔ Post / Content | 게시글 관심 신호 입력 | `Personalization Profile` | `Post / Content` | **CS** | **OHS/PL** | 게시글 메타데이터 projection / 경량 포트 | `UserProfileService`가 `PostKeyword`와 게시글 제목을 직접 읽는다 | 개인화 프로필 생성에 필요한 게시글 신호를 소비한다. 장기적으로는 경량 projection/port로 정리 가능하다. | +| User Account ↔ Personalization Profile | 프로필 생성 트리거 | `Personalization Profile` | `User Account` | **동기 직접 호출** | **OHS/PL** (Open Host Service / Published Language) | 도메인 이벤트 / Published Language | 현재는 `UserCommandService`, `InterestCommandService`가 `PersonalizationProfileService`를 직접 호출 | 목표 상태는 `OnboardingCompleted`, `UserInterestsChanged` 같은 이벤트 handoff다. | +| Personalization Profile ↔ Activity | 행동 요약 입력 | `Personalization Profile` | `Activity` | **CS** | **OHS/PL** | 조회 포트 / `UserActivitySummary` 같은 Published Language | `PersonalizationProfileService`가 Activity repository를 직접 조회 | 이 seam은 현재는 Customer-Supplier에 가깝지만, 목표 상태는 “행동 요약 언어를 소비한다”는 OHS/PL이 더 자연스럽다. | +| Personalization Profile ↔ Post / Content | 게시글 관심 신호 입력 | `Personalization Profile` | `Post / Content` | **CS** | **OHS/PL** | 게시글 메타데이터 projection / 경량 포트 | `PersonalizationProfileService`가 `PostKeyword`와 게시글 제목을 직접 읽는다 | 개인화 프로필 생성에 필요한 게시글 신호를 소비한다. 장기적으로는 경량 projection/port로 정리 가능하다. | | Personalization Profile ↔ Recommendation | 프로필 생성 완료 handoff | `Recommendation` | `Personalization Profile` | **강한 CS + 동기 직접 호출** | **OHS/PL** | 프로필 생성 완료 이벤트 | 현재는 개인화 프로필 생성 직후 추천 생성을 직접 호출 | 목표 상태는 `PersonalizedProfileGenerated` 이벤트로 추천 재생성을 트리거하는 것이다. | | Search ↔ Post / Content | 검색용 게시글 projection | `Search` | `Post / Content` | **OHS/PL** | **OHS/PL** (Open Host Service / Published Language) | Read Model / Projection 소비 | `PostDocument`, `PostRepository` 참조 | Post가 `PostDocument` projection을 제공하고 Search가 이를 소비한다. | -| Search ↔ Personalization Profile | 개인화 리랭킹 입력 | `Search` | `Personalization Profile` | **OHS/PL** | **OHS/PL** (Open Host Service / Published Language) | Read Model / Projection 소비 | `UserProfileDocument` 참조 | Search는 `UserProfileDocument`를 소비해 개인화 리랭킹을 수행한다. | +| Search ↔ Personalization Profile | 개인화 리랭킹 입력 | `Search` | `Personalization Profile` | **OHS/PL** | **OHS/PL** (Open Host Service / Published Language) | Read Model / Projection 소비 | `PersonalizationProfileDocument` 참조 | Search는 `PersonalizationProfileDocument`를 소비해 개인화 리랭킹을 수행한다. | | Search ↔ Activity | 북마크 여부 조회 | `Search` | `Activity` | **CS** | **CS** (Customer-Supplier) | Query Composition | `BookmarkRepository`로 북마크 여부 조회 | 검색 결과 응답 조립을 위한 조회 조합이다. | | Recommendation ↔ User Account | 추천 대상 사용자 식별 | `Recommendation` | `User Account` | **직접 엔티티 참조** (의도상 최소 SK) | **SK** (최소 Shared Identity) | 추천 대상 사용자 식별 | `User` 직접 참조 | 추천 대상 사용자의 정체성과 상태를 알아야 한다. 장기적으로는 최소 사용자 식별자/상태 공유로 축소 가능하다. | -| Recommendation ↔ Personalization Profile | 프로필 벡터/핵심 키워드 입력 | `Recommendation` | `Personalization Profile` | **OHS/PL** | **OHS/PL** (Open Host Service / Published Language) | Read Model 소비 | `UserProfileDocument` 참조 | Recommendation의 핵심 입력은 개인화 프로필 벡터와 핵심 키워드다. | +| Recommendation ↔ Personalization Profile | 프로필 벡터/핵심 키워드 입력 | `Recommendation` | `Personalization Profile` | **OHS/PL** | **OHS/PL** (Open Host Service / Published Language) | Read Model 소비 | `PersonalizationProfileDocument` 참조 | Recommendation의 핵심 입력은 개인화 프로필 벡터와 핵심 키워드다. | | Recommendation ↔ Activity | 읽은 게시글 제외 신호 | `Recommendation` | `Activity` | **CS** | **CS** (Customer-Supplier) | Query Composition / 정책 포트 | `ReadPostRepository`로 읽은 게시글 제외 | 추천 정책에 필요한 제외 신호를 Activity가 공급한다. 장기적으로는 exclusion set 포트로 좁힐 수 있다. | | Recommendation ↔ Post / Content | 추천 후보 탐색 | `Recommendation` | `Post / Content` | **OHS/PL** | **OHS/PL** | 추천 후보 projection 소비 | `PostDocument` 참조 | 추천 후보 탐색은 Post projection 소비로 충분한 것이 목표 상태다. | | Recommendation ↔ Post / Content | 추천 저장 대상 식별 | `Recommendation` | `Post / Content` | **직접 엔티티 참조** | **SK** (최소 Shared Identity) | `PostId`/참조 식별 | 현재는 JPA `Post` reference를 직접 저장 | 후보 탐색 seam과 저장 seam을 분리해서 본다. 저장 쪽은 최소 기술 게시글 식별 공유로 축소하는 것이 목표다. | | Post / User / Personalization Profile / Recommendation / Search ↔ LLM/Embedding Provider | 모델 제공자 연동 | `Post / User / Personalization Profile / Recommendation / Search` | LLM/Embedding Provider | **ACL** | **ACL** (Anti-Corruption Layer) | Port & Adapter | `LlmClient`, `EmbeddingClient` | 외부 모델 제공자를 인터페이스로 차단한다. 제공자 변경 시 ACL만 수정하면 된다. | -| Search / Recommendation / Personalization Profile ↔ Elasticsearch | 읽기 모델 저장/조회 | `Search / Recommendation / Personalization Profile` | Elasticsearch | **읽기 모델 인프라 + ACL 성격** | **ACL** (Anti-Corruption Layer) | Projection / Search Read Model | `PostDocument`, `UserProfileDocument` | 검색/추천/개인화 프로필용 읽기 모델이다. ES 인덱스 구조 변경이 도메인 모델에 전파되지 않도록 차단한다. | +| Search / Recommendation / Personalization Profile ↔ Elasticsearch | 읽기 모델 저장/조회 | `Search / Recommendation / Personalization Profile` | Elasticsearch | **읽기 모델 인프라 + ACL 성격** | **ACL** (Anti-Corruption Layer) | Projection / Search Read Model | `PostDocument`, `PersonalizationProfileDocument` | 검색/추천/개인화 프로필용 읽기 모델이다. ES 인덱스 구조 변경이 도메인 모델에 전파되지 않도록 차단한다. | | Source / Ops ↔ Discord Webhook | 운영 알림 전송 | `Source / Ops` | Discord Webhook | **ACL** | **ACL** (Anti-Corruption Layer) | External Adapter | `WebhookNotificationService` | 운영 알림용 외부 통합이다. 도메인 핵심 모델과 분리되어야 한다. | ### 3.3 현재 통합 스타일 평가 @@ -242,7 +242,7 @@ U/D 표기: **U** = Upstream(상류, 공급자), **D** = Downstream(하류, 소 - 초기 서비스 규모에서는 빠르게 기능을 연결할 수 있다. - 리스크 - `Source ↔ Post`처럼 양방향 도메인 의존이 생긴다. - - `UserProfileService`가 Activity, Post, Recommendation을 모두 알아 개인화 흐름의 결합도가 높다. + - `PersonalizationProfileService`가 Activity, Post, Recommendation을 모두 알아 개인화 흐름의 결합도가 높다. - Search/Recommendation이 여러 컨텍스트의 repository/read model을 직접 조합한다. - 개선 우선순위 1. `PersonalizedProfileGenerated` 이벤트로 **개인화 프로필 생성**과 추천 생성을 분리한다. @@ -268,7 +268,7 @@ U/D 표기: **U** = Upstream(상류, 공급자), **D** = Downstream(하류, 소 [Personalization Profile] 사용자 + 활동 데이터 + 게시글 신호 - → 개인화 프로필(UserProfileDocument: profileText, profileVector, keyKeywords) + → 개인화 프로필(PersonalizationProfileDocument: profileText, profileVector, keyKeywords) [Activity] 읽은 게시글(ReadPost) / 검색 기록(SearchHistory) / 북마크(Bookmark) diff --git a/docs/test-gap-analysis.md b/docs/test-gap-analysis.md index 65a03250..27aed7dd 100644 --- a/docs/test-gap-analysis.md +++ b/docs/test-gap-analysis.md @@ -17,7 +17,7 @@ | Source / Ingestion | RSS reader, processor, writer, scheduler, crawling service 테스트가 잘 있음 | 파이프라인 보호 수준 좋음 | | Post / Content | 조회 API/repository는 강함. 도메인 엔티티/요약/임베딩 테스트는 부족 | `Post = 기술 게시글` 애그리거트 테스트 필요 | | User Account | 온보딩/관심사/계정 프로필은 강함 | 사용자 계정 애그리거트와 온보딩 흐름은 비교적 잘 보호되어 있다 | -| Personalization Profile | 일반 테스트 안전망이 약함 | `UserProfileService`/`UserProfileDocument` 중심 개인화 흐름 보호가 필요하다 | +| Personalization Profile | 일반 테스트 안전망이 약함 | `PersonalizationProfileService`/`PersonalizationProfileDocument` 중심 개인화 흐름 보호가 필요하다 | | Recommendation | 조회 쪽은 일부 있음. 추천 생성/후보 탐색/MMR/이력화 테스트는 부족 | DDD 전환 전 가장 큰 리스크 중 하나 | | Search | 일반 실행 테스트가 거의 없음. evaluation suite만 존재 | 일반 회귀 테스트 부재가 큼 | | Auth / Security | 토큰/필터/컨트롤러 테스트는 비교적 강함 | OAuth handler/OIDC 일부 보강 여지 | @@ -28,7 +28,7 @@ 1. **SearchServiceImpl 일반 회귀 테스트 부재** 2. **LlmRecommendationService / MmrService 테스트 부재** -3. **Personalization Profile(`UserProfileService`) 일반 테스트 부재** +3. **Personalization Profile(`PersonalizationProfileService`) 일반 테스트 부재** 4. **Post 애그리거트 단위 테스트 부재** 5. **Post embedding pipeline 테스트 부재** @@ -258,35 +258,35 @@ ContentChunkerServiceTest | `InterestCommandServiceTest` | unit/mock | 관심사 저장, 기존 관심사 clear, invalid keyword category | | `UserCommandServiceTest` | unit/mock | 온보딩, 계정 프로필 수정, 탈퇴 | | `UserQueryServiceTest` | unit/mock | 계정 프로필 조회 | -| `evaluation/search/setup/UserProfileServiceTest` | evaluation-setup | 테스트 사용자 프로필 생성용 setup | +| `evaluation/search/setup/PersonalizationProfileServiceTest` | evaluation-setup | 테스트 사용자 프로필 생성용 setup | #### 평가 - **User Account 쪽**은 온보딩, 관심사, 계정 프로필, 탈퇴 흐름이 비교적 잘 보호되어 있다. -- 반면 **Personalization Profile 쪽**은 일반 테스트 lane에 `UserProfileService` 안전망이 거의 없다. -- 현재 있는 `UserProfileServiceTest`는 evaluation setup 용도라서, 개인화 프로필 리팩터링 안전망으로 보기 어렵다. +- 반면 **Personalization Profile 쪽**은 일반 테스트 lane에 `PersonalizationProfileService` 안전망이 거의 없다. +- 현재 있는 `PersonalizationProfileServiceTest`는 evaluation setup 용도라서, 개인화 프로필 리팩터링 안전망으로 보기 어렵다. #### 남은 갭 | 우선순위 | 갭 | 이유 | |---|---|---| -| P0 | `UserProfileServiceTest` 일반 단위 테스트 | Personalization Profile 생성은 추천/검색 개인화의 핵심이며 결합도가 높음 | +| P0 | `PersonalizationProfileServiceTest` 일반 단위 테스트 | Personalization Profile 생성은 추천/검색 개인화의 핵심이며 결합도가 높음 | | P0 | LLM 응답 parsing 테스트 | `### PROFILE`, `### KEYWORDS` parsing 실패 시 품질/장애 영향 | | P0 | 관심사 변경 후 개인화 프로필 생성 트리거 검증 | `UserInterestsChanged` 이벤트 도입 전 현재 동작 보호 | | P1 | `UserTest` | User Account aggregate의 소셜 사용자 생성, 온보딩 ACTIVE, 탈퇴 anonymization, reactivate 규칙 보호 | | P1 | `UserInterestCategory/UserInterestKeyword` 도메인 테스트 | 관심 키워드가 카테고리에 속해야 한다는 규칙 명시 | -| P1 | `UserProfileSchedulerTest` | 매일 06:00 KST active user personalization profile regeneration 보호 | +| P1 | `PersonalizationProfileSchedulerTest` | 매일 06:00 KST active user personalization profile regeneration 보호 | | P2 | `InterestQueryServiceTest` | 관심사 조회 변환 로직 보호 | #### 추천 추가 테스트 ```text -UserProfileServiceTest +PersonalizationProfileServiceTest - 관심사, 읽은 게시글, 북마크, 검색 기록을 활동 데이터로 수집한다. - 읽은 시간은 읽기 몰입도 자연어로 변환된다. - LLM 응답에서 profileText와 keyKeywords를 파싱한다. - 파싱 실패 시 fallback 정책을 따른다. -- profileText를 임베딩하여 UserProfileDocument를 저장한다. +- profileText를 임베딩하여 PersonalizationProfileDocument를 저장한다. - 개인화 프로필 생성 후 추천 생성을 호출한다. - 추천 생성 실패가 개인화 프로필 저장을 깨뜨리지 않는지 정책을 검증한다. ``` @@ -316,7 +316,7 @@ UserTest #### 평가 조회 쪽은 일부 보호되어 있지만, 핵심인 **추천 생성 로직이 거의 비어 있다.** -`LlmRecommendationService`는 Personalization Profile(`UserProfileDocument`), Elasticsearch, Post, Activity, RRF, MMR, time decay, history 저장을 모두 다루므로 DDD 리팩터링 전 반드시 테스트가 필요하다. +`LlmRecommendationService`는 Personalization Profile(`PersonalizationProfileDocument`), Elasticsearch, Post, Activity, RRF, MMR, time decay, history 저장을 모두 다루므로 DDD 리팩터링 전 반드시 테스트가 필요하다. #### 남은 갭 @@ -387,7 +387,7 @@ evaluation suite는 무겁고 runtime/profile/fixture 의존이 강하므로 DDD |---|---|---| | P0 | `SearchServiceImplTest` 일반 단위 테스트 | 일반 검색/개인화 검색 핵심 흐름 보호 필요 | | P0 | RRF 결합 테스트 | 검색 결과 순위 품질과 직접 연결 | -| P0 | 개인화 fallback/reranking 테스트 | Personalization Profile(`UserProfileDocument`) 경계 정리 전 필요 | +| P0 | 개인화 fallback/reranking 테스트 | Personalization Profile(`PersonalizationProfileDocument`) 경계 정리 전 필요 | | P1 | BM25 query builder 구조 테스트 | dis_max/exact/fuzzy/chunk 구조 회귀 방지 | | P1 | Semantic KNN field/boost 테스트 | title/summary/chunk embedding field 회귀 방지 | | P1 | metadata attachment 테스트 | viewCount, isBookmarked 조합 보호 | @@ -501,7 +501,7 @@ src/main/java/com/techfork/domain/notification/entity/NotificationToken.java | `PostKeyword` | 직접 테스트 없음 | `Post` 내부 엔티티로 테스트하면 충분 | | `User` | `UserCommandServiceTest` 중심 | User Account aggregate 관점의 직접 `UserTest` 필요 | | `UserInterestCategory/Keyword` | repository/service 중심 | User Account 도메인 규칙 테스트 보강 필요 | -| `UserProfileDocument` | evaluation setup 중심 | Personalization Profile projection 생성/파싱 일반 테스트 필요 | +| `PersonalizationProfileDocument` | evaluation setup 중심 | Personalization Profile projection 생성/파싱 일반 테스트 필요 | | `ReadPost` | service/repository 중심 | record aggregate 단위 테스트는 선택 | | `Bookmark` | 현재 `ScrabPost` service/repository 중심 | rename 후 `Bookmark` 단위 테스트 필요 | | `SearchHistory` | repository/service 중심 | record aggregate 단위 테스트는 선택 | @@ -518,7 +518,7 @@ src/main/java/com/techfork/domain/notification/entity/NotificationToken.java | 테스트 | 목적 | 선행/연결 작업 | |---|---|---| | `PostTest` | `Post = 기술 게시글` 애그리거트 보호 | Post 용어 정리, EDifficultyLevel 제거 | -| `UserProfileServiceTest` | Personalization Profile 생성 흐름 보호 | User Account / Personalization Profile 경계 정리 | +| `PersonalizationProfileServiceTest` | Personalization Profile 생성 흐름 보호 | User Account / Personalization Profile 경계 정리 | | `MmrServiceTest` | 추천 알고리즘 핵심 보호 | Recommendation DDD 전환 | | `LlmRecommendationServiceTest` | 추천 생성/이력화/읽은 글 제외 보호 | `PersonalizedProfileGenerated` 이벤트 도입 전 | | `SearchServiceImplTest` | 검색 일반 회귀 안전망 | Search read model/adapter 분리 전 | @@ -537,7 +537,7 @@ src/main/java/com/techfork/domain/notification/entity/NotificationToken.java | `RecommendationSchedulerTest` | 일일 추천 생성 스케줄 보호 | | `RecommendedPostRepositoryTest` | rank order/delete/unique 보호 | | `RecommendationHistoryTest` | 이력화/click 기록 보호 | -| `UserProfileSchedulerTest` | 일일 개인화 프로필 재생성 보호 | +| `PersonalizationProfileSchedulerTest` | 일일 개인화 프로필 재생성 보호 | | `SearchControllerIntegrationTest` | 검색 API contract 보호 | | `PostKeywordRepositoryTest` | 키워드 조회 조합 보호 | @@ -570,7 +570,7 @@ src/main/java/com/techfork/domain/notification/entity/NotificationToken.java 2. ScrabPost → Bookmark rename 전후 테스트 안정화 3. PostTest 작성 4. Post embedding pipeline 테스트 작성 -5. UserProfileServiceTest 작성 +5. PersonalizationProfileServiceTest 작성 6. MmrServiceTest 작성 7. LlmRecommendationServiceTest 작성 8. SearchServiceImplTest 작성 @@ -591,7 +591,7 @@ src/main/java/com/techfork/domain/notification/entity/NotificationToken.java - EDifficultyLevel 제거 전 사용처 확인 작업 3: Personalization Profile 테스트 -- UserProfileServiceTest 추가 +- PersonalizationProfileServiceTest 추가 - LLM/Embedding/RecommendationService는 mock 처리 작업 4: 추천 생성 테스트 @@ -646,9 +646,9 @@ src/test/java/com/techfork/domain service UserCommandServiceTest InterestCommandServiceTest - UserProfileServiceTest + PersonalizationProfileServiceTest scheduler - UserProfileSchedulerTest + PersonalizationProfileSchedulerTest recommendation entity @@ -685,7 +685,7 @@ P0 테스트가 모두 존재한다. ./gradlew test -PexcludeIntegration 통과. Activity/Bookmark rename 후 기존 Activity 테스트 통과. PostTest로 기술 게시글 애그리거트 기본 규칙 보호. -UserProfileServiceTest로 Personalization Profile 생성 흐름 보호. +PersonalizationProfileServiceTest로 Personalization Profile 생성 흐름 보호. MmrServiceTest와 LlmRecommendationServiceTest로 추천 생성 핵심 흐름 보호. SearchServiceImplTest로 일반/개인화 검색 회귀 보호. ``` diff --git a/docs/ubiquitous-language/README.md b/docs/ubiquitous-language/README.md index 6f9a681a..83dd535f 100644 --- a/docs/ubiquitous-language/README.md +++ b/docs/ubiquitous-language/README.md @@ -52,13 +52,13 @@ | 표준 용어 | 현재 코드상 표현 | 의미 | |---|---|---| | 계정 프로필 | `User.nickName`, `User.description`, `User.profileImage` | 사용자에게 보이는 기본 프로필 정보 | -| 개인화 프로필 | `UserProfileDocument.profileText`, `profileVector` | 검색 리랭킹/추천용 활동 기반 프로필 | +| 개인화 프로필 | `PersonalizationProfileDocument.profileText`, `profileVector` | 검색 리랭킹/추천용 활동 기반 프로필 | 규칙: - 문서/PR/API에서 **“프로필” 단독 표현은 지양**한다. - UI/설정 화면은 `계정 프로필 수정`, 추천/검색 준비 상태는 `개인화 프로필 생성/재생성`으로 쓴다. -- `UserProfileDocument`는 현재 `domain/user` 패키지에 있으나, 개념상으로는 `Personalization Profile` language zone의 read model이다. +- `PersonalizationProfileDocument`는 현재 `domain/user` 패키지에 있으나, 개념상으로는 `Personalization Profile` language zone의 read model이다. --- diff --git a/docs/ubiquitous-language/personalization-profile.md b/docs/ubiquitous-language/personalization-profile.md index 68d21312..b6e1c1ce 100644 --- a/docs/ubiquitous-language/personalization-profile.md +++ b/docs/ubiquitous-language/personalization-profile.md @@ -12,7 +12,7 @@ | 용어 | 코드상 표현 | 정의 | |---|---|---| -| 개인화 프로필 | `UserProfileDocument` | 사용자 활동 데이터를 LLM으로 요약하고 임베딩한 개인화용 프로필 문서 | +| 개인화 프로필 | `PersonalizationProfileDocument` | 사용자 활동 데이터를 LLM으로 요약하고 임베딩한 개인화용 프로필 문서 | | 프로필 텍스트 | `profileText` | 검색 리랭킹과 추천에 사용할 사용자 관심사 설명문 | | 프로필 벡터 | `profileVector` | `profileText`를 임베딩한 벡터 | | 핵심 키워드 | `keyKeywords` | LLM이 사용자 활동에서 추출한 3~5개 대표 관심 키워드 | @@ -23,16 +23,16 @@ | 내부 용어 | 코드상 표현 | 설명 | |---|---|---| -| 프로필 생성 서비스 | `UserProfileService` | 활동/관심사 데이터를 모아 개인화 프로필 projection을 생성하는 서비스 | -| 프로필 재생성 스케줄러 | `UserProfileScheduler` | 활성 사용자 개인화 프로필을 주기적으로 다시 생성하는 스케줄러 | -| 프로필 projection | `UserProfileDocument` | 검색/추천에 제공되는 read model | +| 프로필 생성 서비스 | `PersonalizationProfileService` | 활동/관심사 데이터를 모아 개인화 프로필 projection을 생성하는 서비스 | +| 프로필 재생성 스케줄러 | `PersonalizationProfileScheduler` | 활성 사용자 개인화 프로필을 주기적으로 다시 생성하는 스케줄러 | +| 프로필 projection | `PersonalizationProfileDocument` | 검색/추천에 제공되는 read model | | 개인화 입력 키워드 | `keyKeywords` | 추천 BM25와 검색 개인화에 활용되는 키워드 | | 프로필 생성 트리거 | 관심사 변경 / 활동 누적 | 현재는 서비스 직접 호출 기반, 장기적으로는 이벤트 분리 대상 | ## 혼동 금지 - `개인화 프로필`은 UI용 내 프로필이 아니다. -- `UserProfileDocument`는 aggregate라기보다 projection/read model이다. +- `PersonalizationProfileDocument`는 aggregate라기보다 projection/read model이다. - `핵심 키워드`는 추천/검색용 프로필 파생 키워드이고, `게시글 키워드(PostKeyword)`나 `검색어(SearchQuery)`와 다르다. ## 금지 표현 / 권장 표현 @@ -40,12 +40,12 @@ | 금지/비권장 표현 | 권장 표현 | 이유 | |---|---|---| | 프로필 | 개인화 프로필 | 계정 프로필과 구분해야 한다 | -| 사용자 프로필 문서 | 개인화 프로필 / `UserProfileDocument` | projection 성격을 분명히 하기 위해 | +| 사용자 프로필 문서 | 개인화 프로필 / `PersonalizationProfileDocument` | projection 성격을 분명히 하기 위해 | | 검색용 프로필 | 개인화 프로필 | Search 한정 모델로 오해하지 않기 위해 | | 관심사 프로필 | 활동 데이터 / 개인화 프로필 | 입력 데이터와 생성 결과를 구분해야 한다 | ## 주요 근거 파일 -- `src/main/java/com/techfork/domain/user/service/UserProfileService.java` -- `src/main/java/com/techfork/domain/user/document/UserProfileDocument.java` -- `src/main/java/com/techfork/domain/user/scheduler/UserProfileScheduler.java` +- `src/main/java/com/techfork/domain/user/service/PersonalizationProfileService.java` +- `src/main/java/com/techfork/domain/user/document/PersonalizationProfileDocument.java` +- `src/main/java/com/techfork/domain/user/scheduler/PersonalizationProfileScheduler.java` diff --git a/docs/ubiquitous-language/search.md b/docs/ubiquitous-language/search.md index b9d1759f..b6dfea3e 100644 --- a/docs/ubiquitous-language/search.md +++ b/docs/ubiquitous-language/search.md @@ -45,7 +45,7 @@ ## 혼동 금지 - `검색 결과`는 저장되는 aggregate가 아니라 계산된 응답이다. -- Search가 `PostDocument`, `UserProfileDocument`를 읽는다고 해서 Post/User aggregate를 소유하는 것은 아니다. +- Search가 `PostDocument`, `PersonalizationProfileDocument`를 읽는다고 해서 Post/User aggregate를 소유하는 것은 아니다. - `검색어(SearchQuery)`와 추천의 `keyKeywords`는 둘 다 문자열이지만 생성 주체가 다르다. ## 금지 표현 / 권장 표현 diff --git a/src/main/java/com/techfork/domain/activity/entity/Bookmark.java b/src/main/java/com/techfork/domain/activity/entity/Bookmark.java index 6033bdda..a97c4828 100644 --- a/src/main/java/com/techfork/domain/activity/entity/Bookmark.java +++ b/src/main/java/com/techfork/domain/activity/entity/Bookmark.java @@ -1,52 +1,52 @@ -package com.techfork.domain.activity.entity; - -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.user.entity.User; -import com.techfork.global.common.BaseEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.PersistenceCreator; - -import java.time.LocalDateTime; - -@Entity -@Table( - name = "bookmarks", - uniqueConstraints = { - @UniqueConstraint(name = "uk_bookmarks_user_post", columnNames = {"user_id", "post_id"}) - } -) -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Bookmark extends BaseEntity { - - @Column(name = "bookmarked_at") - private LocalDateTime bookmarkedAt; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id", nullable = false) - private Post post; - - @PersistenceCreator - @Builder - Bookmark(User user, Post post, LocalDateTime bookmarkedAt) { - this.user = user; - this.post = post; - this.bookmarkedAt = bookmarkedAt; - } - - public static Bookmark create(User user, Post post, LocalDateTime bookmarkedAt) { - return Bookmark.builder() - .user(user) - .post(post) - .bookmarkedAt(bookmarkedAt) - .build(); - } -} +package com.techfork.domain.activity.entity; + +import com.techfork.domain.post.entity.Post; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.PersistenceCreator; + +import java.time.LocalDateTime; + +@Entity +@Table( + name = "bookmarks", + uniqueConstraints = { + @UniqueConstraint(name = "uk_bookmarks_user_post", columnNames = {"user_id", "post_id"}) + } +) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Bookmark extends BaseEntity { + + @Column(name = "bookmarked_at") + private LocalDateTime bookmarkedAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private Post post; + + @PersistenceCreator + @Builder + Bookmark(User user, Post post, LocalDateTime bookmarkedAt) { + this.user = user; + this.post = post; + this.bookmarkedAt = bookmarkedAt; + } + + public static Bookmark create(User user, Post post, LocalDateTime bookmarkedAt) { + return Bookmark.builder() + .user(user) + .post(post) + .bookmarkedAt(bookmarkedAt) + .build(); + } +} diff --git a/src/main/java/com/techfork/domain/activity/entity/ReadPost.java b/src/main/java/com/techfork/domain/activity/entity/ReadPost.java index 6e7f4495..6e627a62 100644 --- a/src/main/java/com/techfork/domain/activity/entity/ReadPost.java +++ b/src/main/java/com/techfork/domain/activity/entity/ReadPost.java @@ -1,51 +1,51 @@ -package com.techfork.domain.activity.entity; - -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.user.entity.User; -import com.techfork.global.common.BaseEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.PersistenceCreator; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "read_posts") -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ReadPost extends BaseEntity { - - @Column(nullable = false) - private LocalDateTime readAt; - - private Integer readDurationSeconds; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id", nullable = false) - private Post post; - - @PersistenceCreator - @Builder - ReadPost(User user, Post post, LocalDateTime readAt, Integer readDurationSeconds) { - this.user = user; - this.post = post; - this.readAt = readAt; - this.readDurationSeconds = readDurationSeconds; - } - - public static ReadPost create(User user, Post post, LocalDateTime readAt, Integer readDurationSeconds) { - return ReadPost.builder() - .user(user) - .post(post) - .readAt(readAt) - .readDurationSeconds(readDurationSeconds) - .build(); - } -} +package com.techfork.domain.activity.entity; + +import com.techfork.domain.post.entity.Post; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.PersistenceCreator; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "read_posts") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ReadPost extends BaseEntity { + + @Column(nullable = false) + private LocalDateTime readAt; + + private Integer readDurationSeconds; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private Post post; + + @PersistenceCreator + @Builder + ReadPost(User user, Post post, LocalDateTime readAt, Integer readDurationSeconds) { + this.user = user; + this.post = post; + this.readAt = readAt; + this.readDurationSeconds = readDurationSeconds; + } + + public static ReadPost create(User user, Post post, LocalDateTime readAt, Integer readDurationSeconds) { + return ReadPost.builder() + .user(user) + .post(post) + .readAt(readAt) + .readDurationSeconds(readDurationSeconds) + .build(); + } +} diff --git a/src/main/java/com/techfork/domain/activity/entity/SearchHistory.java b/src/main/java/com/techfork/domain/activity/entity/SearchHistory.java index 1ef61a9b..04b816b6 100644 --- a/src/main/java/com/techfork/domain/activity/entity/SearchHistory.java +++ b/src/main/java/com/techfork/domain/activity/entity/SearchHistory.java @@ -1,31 +1,31 @@ -package com.techfork.domain.activity.entity; - -import com.techfork.domain.user.entity.User; -import com.techfork.global.common.BaseEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.PersistenceCreator; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "search_histories") -@Getter +package com.techfork.domain.activity.entity; + +import com.techfork.domain.useraccount.entity.User; +import com.techfork.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.PersistenceCreator; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "search_histories") +@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class SearchHistory extends BaseEntity { @Column(nullable = false, length = 200) private String query; - - private LocalDateTime searchedAt; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; - + + private LocalDateTime searchedAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + @PersistenceCreator @Builder SearchHistory(User user, String query, LocalDateTime searchedAt) { diff --git a/src/main/java/com/techfork/domain/activity/repository/BookmarkRepository.java b/src/main/java/com/techfork/domain/activity/repository/BookmarkRepository.java index 5bdaca4c..33b6bbfd 100644 --- a/src/main/java/com/techfork/domain/activity/repository/BookmarkRepository.java +++ b/src/main/java/com/techfork/domain/activity/repository/BookmarkRepository.java @@ -1,49 +1,49 @@ -package com.techfork.domain.activity.repository; - -import com.techfork.domain.activity.dto.BookmarkDto; -import com.techfork.domain.activity.entity.Bookmark; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.user.entity.User; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.List; -import java.util.Optional; - -public interface BookmarkRepository extends JpaRepository { - - @Query(""" - SELECT new com.techfork.domain.activity.dto.BookmarkDto( - b.id, p.id, p.title, p.shortSummary, p.url, t.companyName, t.logoUrl, - p.publishedAt, p.thumbnailUrl, p.viewCount, null, true - ) - FROM Bookmark b - JOIN b.post p - JOIN p.techBlog t - WHERE b.user = :user - AND (:lastBookmarkId IS NULL OR b.id < :lastBookmarkId) - ORDER BY b.id DESC - """) - List findBookmarksWithCursor( - @Param("user") User user, - @Param("lastBookmarkId") Long lastBookmarkId, - Pageable pageable - ); - - boolean existsByUserAndPost(User user, Post post); - - Optional findByUserAndPost(User user, Post post); - - @Query("SELECT b FROM Bookmark b JOIN FETCH b.post WHERE b.user.id = :userId ORDER BY b.bookmarkedAt DESC") - List findRecentBookmarksByUserId(@Param("userId") Long userId, Pageable pageable); - - @Query(""" - SELECT b.post.id - FROM Bookmark b - WHERE b.user.id = :userId - AND b.post.id IN :postIds - """) - List findBookmarkedPostIds(@Param("userId") Long userId, @Param("postIds") List postIds); -} +package com.techfork.domain.activity.repository; + +import com.techfork.domain.activity.dto.BookmarkDto; +import com.techfork.domain.activity.entity.Bookmark; +import com.techfork.domain.post.entity.Post; +import com.techfork.domain.useraccount.entity.User; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface BookmarkRepository extends JpaRepository { + + @Query(""" + SELECT new com.techfork.domain.activity.dto.BookmarkDto( + b.id, p.id, p.title, p.shortSummary, p.url, t.companyName, t.logoUrl, + p.publishedAt, p.thumbnailUrl, p.viewCount, null, true + ) + FROM Bookmark b + JOIN b.post p + JOIN p.techBlog t + WHERE b.user = :user + AND (:lastBookmarkId IS NULL OR b.id < :lastBookmarkId) + ORDER BY b.id DESC + """) + List findBookmarksWithCursor( + @Param("user") User user, + @Param("lastBookmarkId") Long lastBookmarkId, + Pageable pageable + ); + + boolean existsByUserAndPost(User user, Post post); + + Optional findByUserAndPost(User user, Post post); + + @Query("SELECT b FROM Bookmark b JOIN FETCH b.post WHERE b.user.id = :userId ORDER BY b.bookmarkedAt DESC") + List findRecentBookmarksByUserId(@Param("userId") Long userId, Pageable pageable); + + @Query(""" + SELECT b.post.id + FROM Bookmark b + WHERE b.user.id = :userId + AND b.post.id IN :postIds + """) + List findBookmarkedPostIds(@Param("userId") Long userId, @Param("postIds") List postIds); +} diff --git a/src/main/java/com/techfork/domain/activity/repository/ReadPostRepository.java b/src/main/java/com/techfork/domain/activity/repository/ReadPostRepository.java index 4370dead..d8b2610d 100644 --- a/src/main/java/com/techfork/domain/activity/repository/ReadPostRepository.java +++ b/src/main/java/com/techfork/domain/activity/repository/ReadPostRepository.java @@ -1,50 +1,50 @@ -package com.techfork.domain.activity.repository; - -import com.techfork.domain.activity.dto.ReadPostDto; -import com.techfork.domain.activity.entity.ReadPost; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.user.entity.User; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.List; - -public interface ReadPostRepository extends JpaRepository { - - boolean existsByUserAndPost(User user, Post post); - - @Query(""" - SELECT rp FROM ReadPost rp - JOIN FETCH rp.post - WHERE rp.user.id = :userId - AND (rp.readDurationSeconds IS NULL OR rp.readDurationSeconds > 10) - ORDER BY rp.readAt DESC - """) - List findRecentReadPostsByUserIdWithMinDuration(@Param("userId") Long userId, Pageable pageable); - - @Query(""" - SELECT new com.techfork.domain.activity.dto.ReadPostDto( - rp.id, p.id, p.title, p.shortSummary, p.url, t.companyName, t.logoUrl, - p.publishedAt, p.thumbnailUrl, p.viewCount, null, null, rp.readAt - ) - FROM ReadPost rp - JOIN rp.post p - JOIN p.techBlog t - WHERE rp.user.id = :userId - AND rp.id IN ( - SELECT MAX(rp2.id) - FROM ReadPost rp2 - WHERE rp2.user.id = :userId - GROUP BY rp2.post.id - ) - AND (:lastReadPostId IS NULL OR rp.id < :lastReadPostId) - ORDER BY rp.id DESC - """) - List findReadPostsWithCursor( - @Param("userId") Long userId, - @Param("lastReadPostId") Long lastReadPostId, - Pageable pageable - ); -} +package com.techfork.domain.activity.repository; + +import com.techfork.domain.activity.dto.ReadPostDto; +import com.techfork.domain.activity.entity.ReadPost; +import com.techfork.domain.post.entity.Post; +import com.techfork.domain.useraccount.entity.User; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ReadPostRepository extends JpaRepository { + + boolean existsByUserAndPost(User user, Post post); + + @Query(""" + SELECT rp FROM ReadPost rp + JOIN FETCH rp.post + WHERE rp.user.id = :userId + AND (rp.readDurationSeconds IS NULL OR rp.readDurationSeconds > 10) + ORDER BY rp.readAt DESC + """) + List findRecentReadPostsByUserIdWithMinDuration(@Param("userId") Long userId, Pageable pageable); + + @Query(""" + SELECT new com.techfork.domain.activity.dto.ReadPostDto( + rp.id, p.id, p.title, p.shortSummary, p.url, t.companyName, t.logoUrl, + p.publishedAt, p.thumbnailUrl, p.viewCount, null, null, rp.readAt + ) + FROM ReadPost rp + JOIN rp.post p + JOIN p.techBlog t + WHERE rp.user.id = :userId + AND rp.id IN ( + SELECT MAX(rp2.id) + FROM ReadPost rp2 + WHERE rp2.user.id = :userId + GROUP BY rp2.post.id + ) + AND (:lastReadPostId IS NULL OR rp.id < :lastReadPostId) + ORDER BY rp.id DESC + """) + List findReadPostsWithCursor( + @Param("userId") Long userId, + @Param("lastReadPostId") Long lastReadPostId, + Pageable pageable + ); +} diff --git a/src/main/java/com/techfork/domain/activity/service/ActivityCommandService.java b/src/main/java/com/techfork/domain/activity/service/ActivityCommandService.java index f926fe21..28c9dd2f 100644 --- a/src/main/java/com/techfork/domain/activity/service/ActivityCommandService.java +++ b/src/main/java/com/techfork/domain/activity/service/ActivityCommandService.java @@ -1,70 +1,70 @@ -package com.techfork.domain.activity.service; - -import com.techfork.domain.activity.dto.BookmarkRequest; -import com.techfork.domain.activity.dto.ReadPostRequest; -import com.techfork.domain.activity.dto.SearchHistoryRequest; -import com.techfork.domain.activity.entity.ReadPost; -import com.techfork.domain.activity.entity.Bookmark; -import com.techfork.domain.activity.entity.SearchHistory; -import com.techfork.domain.activity.exception.ActivityErrorCode; -import com.techfork.domain.activity.repository.ReadPostRepository; -import com.techfork.domain.activity.repository.BookmarkRepository; -import com.techfork.domain.activity.repository.SearchHistoryRepository; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.exception.PostErrorCode; -import com.techfork.domain.post.repository.PostRepository; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.exception.UserErrorCode; -import com.techfork.domain.user.repository.UserRepository; -import com.techfork.global.exception.GeneralException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; - -@Slf4j -@Service -@RequiredArgsConstructor -public class ActivityCommandService { - - private final ReadPostRepository readPostRepository; - private final PostRepository postRepository; - private final UserRepository userRepository; - private final SearchHistoryRepository searchHistoryRepository; - private final BookmarkRepository bookmarkRepository; - - @Transactional - public void saveReadPost(Long userId, ReadPostRequest request) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); - - Post post = postRepository.findById(request.postId()) - .orElseThrow(() -> new GeneralException(PostErrorCode.POST_NOT_FOUND)); - - boolean isFirstRead = !readPostRepository.existsByUserAndPost(user, post); - if (isFirstRead) { - post.incrementViewCount(); - } - - ReadPost readPost = ReadPost.create( - user, - post, - request.readAt(), - request.readDurationSeconds() - ); - - readPostRepository.save(readPost); - log.info("Saved read post for user {} and post {} (viewCount incremented: {})", - userId, request.postId(), isFirstRead); - } - - @Transactional - public void saveSearchHistory(Long userId, SearchHistoryRequest request) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); - +package com.techfork.domain.activity.service; + +import com.techfork.domain.activity.dto.BookmarkRequest; +import com.techfork.domain.activity.dto.ReadPostRequest; +import com.techfork.domain.activity.dto.SearchHistoryRequest; +import com.techfork.domain.activity.entity.ReadPost; +import com.techfork.domain.activity.entity.Bookmark; +import com.techfork.domain.activity.entity.SearchHistory; +import com.techfork.domain.activity.exception.ActivityErrorCode; +import com.techfork.domain.activity.repository.ReadPostRepository; +import com.techfork.domain.activity.repository.BookmarkRepository; +import com.techfork.domain.activity.repository.SearchHistoryRepository; +import com.techfork.domain.post.entity.Post; +import com.techfork.domain.post.exception.PostErrorCode; +import com.techfork.domain.post.repository.PostRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.exception.UserErrorCode; +import com.techfork.domain.useraccount.repository.UserRepository; +import com.techfork.global.exception.GeneralException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ActivityCommandService { + + private final ReadPostRepository readPostRepository; + private final PostRepository postRepository; + private final UserRepository userRepository; + private final SearchHistoryRepository searchHistoryRepository; + private final BookmarkRepository bookmarkRepository; + + @Transactional + public void saveReadPost(Long userId, ReadPostRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); + + Post post = postRepository.findById(request.postId()) + .orElseThrow(() -> new GeneralException(PostErrorCode.POST_NOT_FOUND)); + + boolean isFirstRead = !readPostRepository.existsByUserAndPost(user, post); + if (isFirstRead) { + post.incrementViewCount(); + } + + ReadPost readPost = ReadPost.create( + user, + post, + request.readAt(), + request.readDurationSeconds() + ); + + readPostRepository.save(readPost); + log.info("Saved read post for user {} and post {} (viewCount incremented: {})", + userId, request.postId(), isFirstRead); + } + + @Transactional + public void saveSearchHistory(Long userId, SearchHistoryRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); + SearchHistory searchHistory = SearchHistory.create( user, request.query(), @@ -74,38 +74,38 @@ public void saveSearchHistory(Long userId, SearchHistoryRequest request) { searchHistoryRepository.save(searchHistory); log.info("Saved search history for user {} with query: {}", userId, request.query()); } - - @Transactional - public void addBookmark(Long userId, BookmarkRequest request) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); - - Post post = postRepository.findById(request.postId()) - .orElseThrow(() -> new GeneralException(PostErrorCode.POST_NOT_FOUND)); - - if (bookmarkRepository.existsByUserAndPost(user, post)) { - throw new GeneralException(ActivityErrorCode.BOOKMARK_ALREADY_EXISTS); - } - - Bookmark bookmark = Bookmark.create(user, post, LocalDateTime.now()); - bookmarkRepository.save(bookmark); - - log.info("Saved bookmark for user {} and post {}", userId, request.postId()); - } - - @Transactional - public void deleteBookmark(Long userId, BookmarkRequest request) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); - - Post post = postRepository.findById(request.postId()) - .orElseThrow(() -> new GeneralException(PostErrorCode.POST_NOT_FOUND)); - - Bookmark bookmark = bookmarkRepository.findByUserAndPost(user, post) - .orElseThrow(() -> new GeneralException(ActivityErrorCode.BOOKMARK_NOT_FOUND)); - - bookmarkRepository.delete(bookmark); - log.info("Deleted bookmark for user {} and post {}", userId, request.postId()); - } - -} + + @Transactional + public void addBookmark(Long userId, BookmarkRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); + + Post post = postRepository.findById(request.postId()) + .orElseThrow(() -> new GeneralException(PostErrorCode.POST_NOT_FOUND)); + + if (bookmarkRepository.existsByUserAndPost(user, post)) { + throw new GeneralException(ActivityErrorCode.BOOKMARK_ALREADY_EXISTS); + } + + Bookmark bookmark = Bookmark.create(user, post, LocalDateTime.now()); + bookmarkRepository.save(bookmark); + + log.info("Saved bookmark for user {} and post {}", userId, request.postId()); + } + + @Transactional + public void deleteBookmark(Long userId, BookmarkRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); + + Post post = postRepository.findById(request.postId()) + .orElseThrow(() -> new GeneralException(PostErrorCode.POST_NOT_FOUND)); + + Bookmark bookmark = bookmarkRepository.findByUserAndPost(user, post) + .orElseThrow(() -> new GeneralException(ActivityErrorCode.BOOKMARK_NOT_FOUND)); + + bookmarkRepository.delete(bookmark); + log.info("Deleted bookmark for user {} and post {}", userId, request.postId()); + } + +} diff --git a/src/main/java/com/techfork/domain/activity/service/ActivityQueryService.java b/src/main/java/com/techfork/domain/activity/service/ActivityQueryService.java index bea27641..2372f4ac 100644 --- a/src/main/java/com/techfork/domain/activity/service/ActivityQueryService.java +++ b/src/main/java/com/techfork/domain/activity/service/ActivityQueryService.java @@ -1,158 +1,158 @@ -package com.techfork.domain.activity.service; - -import com.techfork.domain.activity.converter.ActivityConverter; -import com.techfork.domain.activity.dto.BookmarkDto; -import com.techfork.domain.activity.dto.BookmarkListResponse; -import com.techfork.domain.activity.dto.ReadPostDto; -import com.techfork.domain.activity.dto.ReadPostListResponse; -import com.techfork.domain.activity.repository.ReadPostRepository; -import com.techfork.domain.activity.repository.BookmarkRepository; -import com.techfork.domain.post.entity.PostKeyword; -import com.techfork.domain.post.repository.PostKeywordRepository; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.exception.UserErrorCode; -import com.techfork.domain.user.repository.UserRepository; -import com.techfork.global.exception.GeneralException; -import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class ActivityQueryService { - - private final UserRepository userRepository; - private final BookmarkRepository bookmarkRepository; - private final PostKeywordRepository postKeywordRepository; - private final ReadPostRepository readPostRepository; - private final ActivityConverter activityConverter; - private final CloudflareThirdPartyThumbnailOptimizer thumbnailOptimizer; - - public BookmarkListResponse getBookmarks(Long userId, Long lastBookmarkId, int size) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); - - PageRequest pageRequest = PageRequest.of(0, size + 1); - List bookmarks = bookmarkRepository.findBookmarksWithCursor(user, lastBookmarkId, pageRequest); - List bookmarksWithKeywords = attachKeywordsToPostInfoList(bookmarks); - - return activityConverter.toBookmarkListResponse(bookmarksWithKeywords, size); - } - - public ReadPostListResponse getReadPosts(Long userId, Long lastReadPostId, int size) { - PageRequest pageRequest = PageRequest.of(0, size + 1); - List readPosts = readPostRepository.findReadPostsWithCursor(userId, lastReadPostId, pageRequest); - List readPostsWithKeywords = attachKeywordsToReadPosts(readPosts); - List readPostsWithBookmarks = attachBookmarksToReadPosts(readPostsWithKeywords, userId); - - return activityConverter.toReadPostListResponse(readPostsWithBookmarks, size); - } - - private List attachKeywordsToPostInfoList(List bookmarks) { - if (bookmarks.isEmpty()) { - return bookmarks; - } - - List postIds = bookmarks.stream() - .map(BookmarkDto::postId) - .toList(); - - Map> keywordMap = postKeywordRepository.findByPostIdIn(postIds) - .stream() - .collect(Collectors.groupingBy( - pk -> pk.getPost().getId(), - Collectors.mapping(PostKeyword::getKeyword, Collectors.toList()) - )); - - return bookmarks.stream() - .map(post -> BookmarkDto.builder() - .bookmarkId(post.bookmarkId()) - .postId(post.postId()) - .title(post.title()) - .shortSummary(post.shortSummary()) - .url(post.url()) - .companyName(post.companyName()) - .logoUrl(post.logoUrl()) - .publishedAt(post.publishedAt()) - .thumbnailUrl(thumbnailOptimizer.optimize(post.thumbnailUrl())) - .viewCount(post.viewCount()) - .keywords(keywordMap.getOrDefault(post.postId(), List.of())) - .isBookmarked(post.isBookmarked()) - .build()) - .toList(); - } - - private List attachKeywordsToReadPosts(List readPosts) { - if (readPosts.isEmpty()) { - return readPosts; - } - - List postIds = readPosts.stream() - .map(ReadPostDto::postId) - .toList(); - - Map> keywordMap = postKeywordRepository.findByPostIdIn(postIds) - .stream() - .collect(Collectors.groupingBy( - pk -> pk.getPost().getId(), - Collectors.mapping(PostKeyword::getKeyword, Collectors.toList()) - )); - - return readPosts.stream() - .map(readPost -> ReadPostDto.builder() - .readPostId(readPost.readPostId()) - .postId(readPost.postId()) - .title(readPost.title()) - .shortSummary(readPost.shortSummary()) - .url(readPost.url()) - .companyName(readPost.companyName()) - .logoUrl(readPost.logoUrl()) - .publishedAt(readPost.publishedAt()) - .thumbnailUrl(thumbnailOptimizer.optimize(readPost.thumbnailUrl())) - .viewCount(readPost.viewCount()) - .keywords(keywordMap.getOrDefault(readPost.postId(), List.of())) - .isBookmarked(null) - .readAt(readPost.readAt()) - .build()) - .toList(); - } - - private List attachBookmarksToReadPosts(List readPosts, Long userId) { - if (readPosts.isEmpty()) { - return readPosts; - } - - List postIds = readPosts.stream() - .map(ReadPostDto::postId) - .toList(); - - List bookmarkedPostIds = bookmarkRepository.findBookmarkedPostIds(userId, postIds); - - return readPosts.stream() - .map(readPost -> ReadPostDto.builder() - .readPostId(readPost.readPostId()) - .postId(readPost.postId()) - .title(readPost.title()) - .shortSummary(readPost.shortSummary()) - .url(readPost.url()) - .companyName(readPost.companyName()) - .logoUrl(readPost.logoUrl()) - .publishedAt(readPost.publishedAt()) - .thumbnailUrl(thumbnailOptimizer.optimize(readPost.thumbnailUrl())) - .viewCount(readPost.viewCount()) - .keywords(readPost.keywords()) - .isBookmarked(bookmarkedPostIds.contains(readPost.postId())) - .readAt(readPost.readAt()) - .build()) - .toList(); - } -} +package com.techfork.domain.activity.service; + +import com.techfork.domain.activity.converter.ActivityConverter; +import com.techfork.domain.activity.dto.BookmarkDto; +import com.techfork.domain.activity.dto.BookmarkListResponse; +import com.techfork.domain.activity.dto.ReadPostDto; +import com.techfork.domain.activity.dto.ReadPostListResponse; +import com.techfork.domain.activity.repository.ReadPostRepository; +import com.techfork.domain.activity.repository.BookmarkRepository; +import com.techfork.domain.post.entity.PostKeyword; +import com.techfork.domain.post.repository.PostKeywordRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.exception.UserErrorCode; +import com.techfork.domain.useraccount.repository.UserRepository; +import com.techfork.global.exception.GeneralException; +import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ActivityQueryService { + + private final UserRepository userRepository; + private final BookmarkRepository bookmarkRepository; + private final PostKeywordRepository postKeywordRepository; + private final ReadPostRepository readPostRepository; + private final ActivityConverter activityConverter; + private final CloudflareThirdPartyThumbnailOptimizer thumbnailOptimizer; + + public BookmarkListResponse getBookmarks(Long userId, Long lastBookmarkId, int size) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); + + PageRequest pageRequest = PageRequest.of(0, size + 1); + List bookmarks = bookmarkRepository.findBookmarksWithCursor(user, lastBookmarkId, pageRequest); + List bookmarksWithKeywords = attachKeywordsToPostInfoList(bookmarks); + + return activityConverter.toBookmarkListResponse(bookmarksWithKeywords, size); + } + + public ReadPostListResponse getReadPosts(Long userId, Long lastReadPostId, int size) { + PageRequest pageRequest = PageRequest.of(0, size + 1); + List readPosts = readPostRepository.findReadPostsWithCursor(userId, lastReadPostId, pageRequest); + List readPostsWithKeywords = attachKeywordsToReadPosts(readPosts); + List readPostsWithBookmarks = attachBookmarksToReadPosts(readPostsWithKeywords, userId); + + return activityConverter.toReadPostListResponse(readPostsWithBookmarks, size); + } + + private List attachKeywordsToPostInfoList(List bookmarks) { + if (bookmarks.isEmpty()) { + return bookmarks; + } + + List postIds = bookmarks.stream() + .map(BookmarkDto::postId) + .toList(); + + Map> keywordMap = postKeywordRepository.findByPostIdIn(postIds) + .stream() + .collect(Collectors.groupingBy( + pk -> pk.getPost().getId(), + Collectors.mapping(PostKeyword::getKeyword, Collectors.toList()) + )); + + return bookmarks.stream() + .map(post -> BookmarkDto.builder() + .bookmarkId(post.bookmarkId()) + .postId(post.postId()) + .title(post.title()) + .shortSummary(post.shortSummary()) + .url(post.url()) + .companyName(post.companyName()) + .logoUrl(post.logoUrl()) + .publishedAt(post.publishedAt()) + .thumbnailUrl(thumbnailOptimizer.optimize(post.thumbnailUrl())) + .viewCount(post.viewCount()) + .keywords(keywordMap.getOrDefault(post.postId(), List.of())) + .isBookmarked(post.isBookmarked()) + .build()) + .toList(); + } + + private List attachKeywordsToReadPosts(List readPosts) { + if (readPosts.isEmpty()) { + return readPosts; + } + + List postIds = readPosts.stream() + .map(ReadPostDto::postId) + .toList(); + + Map> keywordMap = postKeywordRepository.findByPostIdIn(postIds) + .stream() + .collect(Collectors.groupingBy( + pk -> pk.getPost().getId(), + Collectors.mapping(PostKeyword::getKeyword, Collectors.toList()) + )); + + return readPosts.stream() + .map(readPost -> ReadPostDto.builder() + .readPostId(readPost.readPostId()) + .postId(readPost.postId()) + .title(readPost.title()) + .shortSummary(readPost.shortSummary()) + .url(readPost.url()) + .companyName(readPost.companyName()) + .logoUrl(readPost.logoUrl()) + .publishedAt(readPost.publishedAt()) + .thumbnailUrl(thumbnailOptimizer.optimize(readPost.thumbnailUrl())) + .viewCount(readPost.viewCount()) + .keywords(keywordMap.getOrDefault(readPost.postId(), List.of())) + .isBookmarked(null) + .readAt(readPost.readAt()) + .build()) + .toList(); + } + + private List attachBookmarksToReadPosts(List readPosts, Long userId) { + if (readPosts.isEmpty()) { + return readPosts; + } + + List postIds = readPosts.stream() + .map(ReadPostDto::postId) + .toList(); + + List bookmarkedPostIds = bookmarkRepository.findBookmarkedPostIds(userId, postIds); + + return readPosts.stream() + .map(readPost -> ReadPostDto.builder() + .readPostId(readPost.readPostId()) + .postId(readPost.postId()) + .title(readPost.title()) + .shortSummary(readPost.shortSummary()) + .url(readPost.url()) + .companyName(readPost.companyName()) + .logoUrl(readPost.logoUrl()) + .publishedAt(readPost.publishedAt()) + .thumbnailUrl(thumbnailOptimizer.optimize(readPost.thumbnailUrl())) + .viewCount(readPost.viewCount()) + .keywords(readPost.keywords()) + .isBookmarked(bookmarkedPostIds.contains(readPost.postId())) + .readAt(readPost.readAt()) + .build()) + .toList(); + } +} diff --git a/src/main/java/com/techfork/domain/auth/converter/AuthConverter.java b/src/main/java/com/techfork/domain/auth/converter/AuthConverter.java index b1c4f84e..9128836b 100644 --- a/src/main/java/com/techfork/domain/auth/converter/AuthConverter.java +++ b/src/main/java/com/techfork/domain/auth/converter/AuthConverter.java @@ -2,7 +2,7 @@ import com.techfork.domain.auth.dto.DeveloperTokenResponse; import com.techfork.domain.auth.dto.KakaoLoginResponse; -import com.techfork.domain.user.entity.User; +import com.techfork.domain.useraccount.entity.User; import org.springframework.stereotype.Component; @Component diff --git a/src/main/java/com/techfork/domain/auth/service/AuthService.java b/src/main/java/com/techfork/domain/auth/service/AuthService.java index a4cb36e6..1c3c3ec0 100644 --- a/src/main/java/com/techfork/domain/auth/service/AuthService.java +++ b/src/main/java/com/techfork/domain/auth/service/AuthService.java @@ -6,10 +6,10 @@ import com.techfork.domain.auth.dto.TokenRefreshResponse; import com.techfork.domain.auth.dto.kakao.KakaoUserInfoResponse; import com.techfork.domain.auth.exception.AuthErrorCode; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.exception.GeneralException; import com.techfork.global.security.auth.service.RefreshTokenService; import com.techfork.global.security.auth.service.UserAuthCacheService; diff --git a/src/main/java/com/techfork/domain/notification/entity/NotificationToken.java b/src/main/java/com/techfork/domain/notification/entity/NotificationToken.java index 9e40d540..7b70033f 100644 --- a/src/main/java/com/techfork/domain/notification/entity/NotificationToken.java +++ b/src/main/java/com/techfork/domain/notification/entity/NotificationToken.java @@ -1,24 +1,24 @@ -package com.techfork.domain.notification.entity; - -import com.techfork.domain.user.entity.User; -import com.techfork.global.common.BaseTimeEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class NotificationToken extends BaseTimeEntity { - - @Column(nullable = false, length = 500) - private String token; - - @Column(nullable = false) - private Boolean isActive = true; - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - private User user; -} +package com.techfork.domain.notification.entity; + +import com.techfork.domain.useraccount.entity.User; +import com.techfork.global.common.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class NotificationToken extends BaseTimeEntity { + + @Column(nullable = false, length = 500) + private String token; + + @Column(nullable = false) + private Boolean isActive = true; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; +} diff --git a/src/main/java/com/techfork/domain/user/document/PersonalizationProfileDocument.java b/src/main/java/com/techfork/domain/personalization/document/PersonalizationProfileDocument.java similarity index 97% rename from src/main/java/com/techfork/domain/user/document/PersonalizationProfileDocument.java rename to src/main/java/com/techfork/domain/personalization/document/PersonalizationProfileDocument.java index 5c494485..866ba496 100644 --- a/src/main/java/com/techfork/domain/user/document/PersonalizationProfileDocument.java +++ b/src/main/java/com/techfork/domain/personalization/document/PersonalizationProfileDocument.java @@ -1,4 +1,4 @@ -package com.techfork.domain.user.document; +package com.techfork.domain.personalization.document; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; diff --git a/src/main/java/com/techfork/domain/user/repository/PersonalizationProfileDocumentRepository.java b/src/main/java/com/techfork/domain/personalization/repository/PersonalizationProfileDocumentRepository.java similarity index 69% rename from src/main/java/com/techfork/domain/user/repository/PersonalizationProfileDocumentRepository.java rename to src/main/java/com/techfork/domain/personalization/repository/PersonalizationProfileDocumentRepository.java index d9365bcd..05fea5a2 100644 --- a/src/main/java/com/techfork/domain/user/repository/PersonalizationProfileDocumentRepository.java +++ b/src/main/java/com/techfork/domain/personalization/repository/PersonalizationProfileDocumentRepository.java @@ -1,6 +1,6 @@ -package com.techfork.domain.user.repository; +package com.techfork.domain.personalization.repository; -import com.techfork.domain.user.document.PersonalizationProfileDocument; +import com.techfork.domain.personalization.document.PersonalizationProfileDocument; import java.util.Optional; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; diff --git a/src/main/java/com/techfork/domain/user/scheduler/PersonalizationProfileScheduler.java b/src/main/java/com/techfork/domain/personalization/scheduler/PersonalizationProfileScheduler.java similarity index 85% rename from src/main/java/com/techfork/domain/user/scheduler/PersonalizationProfileScheduler.java rename to src/main/java/com/techfork/domain/personalization/scheduler/PersonalizationProfileScheduler.java index 8be25469..d75486c3 100644 --- a/src/main/java/com/techfork/domain/user/scheduler/PersonalizationProfileScheduler.java +++ b/src/main/java/com/techfork/domain/personalization/scheduler/PersonalizationProfileScheduler.java @@ -1,8 +1,8 @@ -package com.techfork.domain.user.scheduler; +package com.techfork.domain.personalization.scheduler; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.repository.UserRepository; -import com.techfork.domain.user.service.PersonalizationProfileService; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.repository.UserRepository; +import com.techfork.domain.personalization.service.PersonalizationProfileService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; diff --git a/src/main/java/com/techfork/domain/user/service/PersonalizationProfileService.java b/src/main/java/com/techfork/domain/personalization/service/PersonalizationProfileService.java similarity index 95% rename from src/main/java/com/techfork/domain/user/service/PersonalizationProfileService.java rename to src/main/java/com/techfork/domain/personalization/service/PersonalizationProfileService.java index 551039e3..d3c26088 100644 --- a/src/main/java/com/techfork/domain/user/service/PersonalizationProfileService.java +++ b/src/main/java/com/techfork/domain/personalization/service/PersonalizationProfileService.java @@ -1,4 +1,4 @@ -package com.techfork.domain.user.service; +package com.techfork.domain.personalization.service; import com.techfork.domain.activity.entity.ReadPost; import com.techfork.domain.activity.entity.Bookmark; @@ -8,13 +8,13 @@ import com.techfork.domain.activity.repository.SearchHistoryRepository; import com.techfork.domain.post.entity.PostKeyword; import com.techfork.domain.recommendation.service.RecommendationService; -import com.techfork.domain.user.document.PersonalizationProfileDocument; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.entity.UserInterestCategory; -import com.techfork.domain.user.exception.UserErrorCode; -import com.techfork.domain.user.repository.UserInterestCategoryRepository; -import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.personalization.document.PersonalizationProfileDocument; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.entity.UserInterestCategory; +import com.techfork.domain.useraccount.exception.UserErrorCode; +import com.techfork.domain.useraccount.repository.UserInterestCategoryRepository; +import com.techfork.domain.personalization.repository.PersonalizationProfileDocumentRepository; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.exception.GeneralException; import com.techfork.global.llm.EmbeddingClient; import com.techfork.global.llm.LlmClient; diff --git a/src/main/java/com/techfork/domain/recommendation/entity/RecommendationHistory.java b/src/main/java/com/techfork/domain/recommendation/entity/RecommendationHistory.java index 7b2e1206..6cff4381 100644 --- a/src/main/java/com/techfork/domain/recommendation/entity/RecommendationHistory.java +++ b/src/main/java/com/techfork/domain/recommendation/entity/RecommendationHistory.java @@ -1,92 +1,92 @@ -package com.techfork.domain.recommendation.entity; - -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.user.entity.User; -import com.techfork.global.common.BaseEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.PersistenceCreator; - -import java.time.LocalDateTime; - -/** - * 추천 이력 엔티티 - * 과거 추천 기록을 보관하여 추천 품질 분석 및 개선에 활용 - */ -@Entity -@Table( - name = "recommendation_history", - indexes = { - @Index(name = "idx_user_recommended_at", columnList = "user_id, recommended_at DESC"), - @Index(name = "idx_recommended_at", columnList = "recommended_at DESC") - } -) -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class RecommendationHistory extends BaseEntity { - - @Column(nullable = false) - private Double similarityScore; - - @Column(nullable = false) - private Double mmrScore; - - @Column(nullable = false) - private Integer rankOrder; - - @Column(nullable = false) - private LocalDateTime recommendedAt; - - @Column(nullable = false) - private Boolean isClicked = false; - - private LocalDateTime clickedAt; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id", nullable = false) - private Post post; - - @PersistenceCreator - @Builder - RecommendationHistory(User user, Post post, Double similarityScore, - Double mmrScore, Integer rankOrder, LocalDateTime recommendedAt, - Boolean isClicked, LocalDateTime clickedAt) { - this.user = user; - this.post = post; - this.similarityScore = similarityScore; - this.mmrScore = mmrScore; - this.rankOrder = rankOrder; - this.recommendedAt = recommendedAt; - this.isClicked = isClicked != null ? isClicked : false; - this.clickedAt = clickedAt; - } - - /** - * RecommendedPost로부터 이력 생성 - */ - public static RecommendationHistory fromRecommendedPost(RecommendedPost recommendedPost) { - return RecommendationHistory.builder() - .user(recommendedPost.getUser()) - .post(recommendedPost.getPost()) - .similarityScore(recommendedPost.getSimilarityScore()) - .mmrScore(recommendedPost.getMmrScore()) - .rankOrder(recommendedPost.getRankOrder()) - .recommendedAt(recommendedPost.getRecommendedAt()) - .build(); - } - - /** - * 클릭 기록 - */ - public void markAsisClicked() { - this.isClicked = true; - this.clickedAt = LocalDateTime.now(); - } -} +package com.techfork.domain.recommendation.entity; + +import com.techfork.domain.post.entity.Post; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.PersistenceCreator; + +import java.time.LocalDateTime; + +/** + * 추천 이력 엔티티 + * 과거 추천 기록을 보관하여 추천 품질 분석 및 개선에 활용 + */ +@Entity +@Table( + name = "recommendation_history", + indexes = { + @Index(name = "idx_user_recommended_at", columnList = "user_id, recommended_at DESC"), + @Index(name = "idx_recommended_at", columnList = "recommended_at DESC") + } +) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RecommendationHistory extends BaseEntity { + + @Column(nullable = false) + private Double similarityScore; + + @Column(nullable = false) + private Double mmrScore; + + @Column(nullable = false) + private Integer rankOrder; + + @Column(nullable = false) + private LocalDateTime recommendedAt; + + @Column(nullable = false) + private Boolean isClicked = false; + + private LocalDateTime clickedAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private Post post; + + @PersistenceCreator + @Builder + RecommendationHistory(User user, Post post, Double similarityScore, + Double mmrScore, Integer rankOrder, LocalDateTime recommendedAt, + Boolean isClicked, LocalDateTime clickedAt) { + this.user = user; + this.post = post; + this.similarityScore = similarityScore; + this.mmrScore = mmrScore; + this.rankOrder = rankOrder; + this.recommendedAt = recommendedAt; + this.isClicked = isClicked != null ? isClicked : false; + this.clickedAt = clickedAt; + } + + /** + * RecommendedPost로부터 이력 생성 + */ + public static RecommendationHistory fromRecommendedPost(RecommendedPost recommendedPost) { + return RecommendationHistory.builder() + .user(recommendedPost.getUser()) + .post(recommendedPost.getPost()) + .similarityScore(recommendedPost.getSimilarityScore()) + .mmrScore(recommendedPost.getMmrScore()) + .rankOrder(recommendedPost.getRankOrder()) + .recommendedAt(recommendedPost.getRecommendedAt()) + .build(); + } + + /** + * 클릭 기록 + */ + public void markAsisClicked() { + this.isClicked = true; + this.clickedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/techfork/domain/recommendation/entity/RecommendedPost.java b/src/main/java/com/techfork/domain/recommendation/entity/RecommendedPost.java index 8af52b54..039a12a1 100644 --- a/src/main/java/com/techfork/domain/recommendation/entity/RecommendedPost.java +++ b/src/main/java/com/techfork/domain/recommendation/entity/RecommendedPost.java @@ -1,72 +1,72 @@ -package com.techfork.domain.recommendation.entity; - -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.user.entity.User; -import com.techfork.global.common.BaseEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.PersistenceCreator; - -import java.time.LocalDateTime; - -@Entity -@Table( - name = "recommended_posts", - uniqueConstraints = { - @UniqueConstraint(columnNames = {"user_id", "post_id"}) - }, - indexes = { - @Index(name = "idx_user_recommended_at", columnList = "user_id, recommended_at DESC") - } -) -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class RecommendedPost extends BaseEntity { - - @Column(nullable = false) - private Double similarityScore; - - @Column(nullable = false) - private Double mmrScore; - - @Column(nullable = false) - private Integer rankOrder; - - @Column(nullable = false) - private LocalDateTime recommendedAt; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id", nullable = false) - private Post post; - - @PersistenceCreator - @Builder - RecommendedPost(User user, Post post, Double similarityScore, - Double mmrScore, Integer rankOrder, LocalDateTime recommendedAt) { - this.user = user; - this.post = post; - this.similarityScore = similarityScore; - this.mmrScore = mmrScore; - this.rankOrder = rankOrder; - this.recommendedAt = recommendedAt; - } - - public static RecommendedPost create(User user, Post post, Double similarityScore, - Double mmrScore, Integer rankOrder) { - return RecommendedPost.builder() - .user(user) - .post(post) - .similarityScore(similarityScore) - .mmrScore(mmrScore) - .rankOrder(rankOrder) - .recommendedAt(LocalDateTime.now()) - .build(); - } -} +package com.techfork.domain.recommendation.entity; + +import com.techfork.domain.post.entity.Post; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.PersistenceCreator; + +import java.time.LocalDateTime; + +@Entity +@Table( + name = "recommended_posts", + uniqueConstraints = { + @UniqueConstraint(columnNames = {"user_id", "post_id"}) + }, + indexes = { + @Index(name = "idx_user_recommended_at", columnList = "user_id, recommended_at DESC") + } +) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RecommendedPost extends BaseEntity { + + @Column(nullable = false) + private Double similarityScore; + + @Column(nullable = false) + private Double mmrScore; + + @Column(nullable = false) + private Integer rankOrder; + + @Column(nullable = false) + private LocalDateTime recommendedAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private Post post; + + @PersistenceCreator + @Builder + RecommendedPost(User user, Post post, Double similarityScore, + Double mmrScore, Integer rankOrder, LocalDateTime recommendedAt) { + this.user = user; + this.post = post; + this.similarityScore = similarityScore; + this.mmrScore = mmrScore; + this.rankOrder = rankOrder; + this.recommendedAt = recommendedAt; + } + + public static RecommendedPost create(User user, Post post, Double similarityScore, + Double mmrScore, Integer rankOrder) { + return RecommendedPost.builder() + .user(user) + .post(post) + .similarityScore(similarityScore) + .mmrScore(mmrScore) + .rankOrder(rankOrder) + .recommendedAt(LocalDateTime.now()) + .build(); + } +} diff --git a/src/main/java/com/techfork/domain/recommendation/repository/RecommendedPostRepository.java b/src/main/java/com/techfork/domain/recommendation/repository/RecommendedPostRepository.java index b1332413..92ca652f 100644 --- a/src/main/java/com/techfork/domain/recommendation/repository/RecommendedPostRepository.java +++ b/src/main/java/com/techfork/domain/recommendation/repository/RecommendedPostRepository.java @@ -1,26 +1,26 @@ -package com.techfork.domain.recommendation.repository; - -import com.techfork.domain.recommendation.entity.RecommendedPost; -import com.techfork.domain.user.entity.User; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.List; - -public interface RecommendedPostRepository extends JpaRepository { - - @Query(""" - SELECT rp FROM RecommendedPost rp - JOIN FETCH rp.post p - JOIN FETCH p.techBlog - WHERE rp.user = :user - ORDER BY rp.rankOrder ASC - """) - List findByUserOrderByRankAsc(@Param("user") User user); - - @Modifying - @Query("DELETE FROM RecommendedPost rp WHERE rp.user = :user") - void deleteByUser(@Param("user") User user); -} +package com.techfork.domain.recommendation.repository; + +import com.techfork.domain.recommendation.entity.RecommendedPost; +import com.techfork.domain.useraccount.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface RecommendedPostRepository extends JpaRepository { + + @Query(""" + SELECT rp FROM RecommendedPost rp + JOIN FETCH rp.post p + JOIN FETCH p.techBlog + WHERE rp.user = :user + ORDER BY rp.rankOrder ASC + """) + List findByUserOrderByRankAsc(@Param("user") User user); + + @Modifying + @Query("DELETE FROM RecommendedPost rp WHERE rp.user = :user") + void deleteByUser(@Param("user") User user); +} diff --git a/src/main/java/com/techfork/domain/recommendation/scheduler/RecommendationScheduler.java b/src/main/java/com/techfork/domain/recommendation/scheduler/RecommendationScheduler.java index 301d1108..738c9575 100644 --- a/src/main/java/com/techfork/domain/recommendation/scheduler/RecommendationScheduler.java +++ b/src/main/java/com/techfork/domain/recommendation/scheduler/RecommendationScheduler.java @@ -1,61 +1,61 @@ -package com.techfork.domain.recommendation.scheduler; - -import com.techfork.domain.recommendation.config.RecommendationProperties; -import com.techfork.domain.recommendation.service.RecommendationService; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import java.time.LocalDateTime; -import java.util.List; - -@Slf4j -@Component -@RequiredArgsConstructor -public class RecommendationScheduler { - - private final UserRepository userRepository; - private final RecommendationService recommendationService; - private final RecommendationProperties properties; - - /** - * 매일 오전 7시(KST)에 활성 사용자 대상 추천 생성 - * - 크롤링(5시) → 프로필 생성(6시) → 추천 생성(7시) 순서 - * - 최근 N시간 이내 활성 사용자만 대상 - * - 향후 추천 알림 기능 추가 예정 - */ - @Scheduled(cron = "0 0 7 * * *", zone = "Asia/Seoul") - public void generateDailyRecommendations() { - log.info("활성 사용자 대상으로 게시글 추천 시작"); - - LocalDateTime since = LocalDateTime.now().minusHours(properties.getActiveUserHours()); - List activeUsers = userRepository.findActiveUsersSince(since); - - log.info("{} 명의 활성 사용자를 찾았습니다. ({} 시간 이내)", activeUsers.size(), properties.getActiveUserHours()); - - int totalRecommendations = 0; - int successCount = 0; - int failCount = 0; - - for (User user : activeUsers) { - try { - int count = recommendationService.generateRecommendationsForUser(user); - totalRecommendations += count; - successCount++; - - if (count > 0) { - log.debug("사용자 {}에게 {} 개 추천 생성 완료", user.getId(), count); - } - } catch (Exception e) { - failCount++; - log.error("사용자 {} 추천 생성 실패", user.getId(), e); - } - } - - log.info("일일 추천 생성 완료: 전체 사용자={}, 성공={}, 실패={}, 총 추천 개수={}", - activeUsers.size(), successCount, failCount, totalRecommendations); - } -} +package com.techfork.domain.recommendation.scheduler; + +import com.techfork.domain.recommendation.config.RecommendationProperties; +import com.techfork.domain.recommendation.service.RecommendationService; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RecommendationScheduler { + + private final UserRepository userRepository; + private final RecommendationService recommendationService; + private final RecommendationProperties properties; + + /** + * 매일 오전 7시(KST)에 활성 사용자 대상 추천 생성 + * - 크롤링(5시) → 프로필 생성(6시) → 추천 생성(7시) 순서 + * - 최근 N시간 이내 활성 사용자만 대상 + * - 향후 추천 알림 기능 추가 예정 + */ + @Scheduled(cron = "0 0 7 * * *", zone = "Asia/Seoul") + public void generateDailyRecommendations() { + log.info("활성 사용자 대상으로 게시글 추천 시작"); + + LocalDateTime since = LocalDateTime.now().minusHours(properties.getActiveUserHours()); + List activeUsers = userRepository.findActiveUsersSince(since); + + log.info("{} 명의 활성 사용자를 찾았습니다. ({} 시간 이내)", activeUsers.size(), properties.getActiveUserHours()); + + int totalRecommendations = 0; + int successCount = 0; + int failCount = 0; + + for (User user : activeUsers) { + try { + int count = recommendationService.generateRecommendationsForUser(user); + totalRecommendations += count; + successCount++; + + if (count > 0) { + log.debug("사용자 {}에게 {} 개 추천 생성 완료", user.getId(), count); + } + } catch (Exception e) { + failCount++; + log.error("사용자 {} 추천 생성 실패", user.getId(), e); + } + } + + log.info("일일 추천 생성 완료: 전체 사용자={}, 성공={}, 실패={}, 총 추천 개수={}", + activeUsers.size(), successCount, failCount, totalRecommendations); + } +} diff --git a/src/main/java/com/techfork/domain/recommendation/service/LlmRecommendationService.java b/src/main/java/com/techfork/domain/recommendation/service/LlmRecommendationService.java index 19adac2f..b7b3b8ea 100644 --- a/src/main/java/com/techfork/domain/recommendation/service/LlmRecommendationService.java +++ b/src/main/java/com/techfork/domain/recommendation/service/LlmRecommendationService.java @@ -17,9 +17,9 @@ import com.techfork.domain.recommendation.repository.RecommendationHistoryRepository; import com.techfork.domain.recommendation.service.MmrService.MmrCandidate; import com.techfork.domain.recommendation.service.MmrService.MmrResult; -import com.techfork.domain.user.document.PersonalizationProfileDocument; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; +import com.techfork.domain.personalization.document.PersonalizationProfileDocument; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.personalization.repository.PersonalizationProfileDocumentRepository; import com.techfork.global.util.RrfScorer; import com.techfork.global.util.TimeDecayStrategy; import com.techfork.global.util.VectorUtil; diff --git a/src/main/java/com/techfork/domain/recommendation/service/RecommendationCommandService.java b/src/main/java/com/techfork/domain/recommendation/service/RecommendationCommandService.java index 9a34b967..77652430 100644 --- a/src/main/java/com/techfork/domain/recommendation/service/RecommendationCommandService.java +++ b/src/main/java/com/techfork/domain/recommendation/service/RecommendationCommandService.java @@ -1,24 +1,24 @@ -package com.techfork.domain.recommendation.service; - -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Slf4j -@Service -@Transactional -@RequiredArgsConstructor -public class RecommendationCommandService { - - private final RecommendationService recommendationService; - private final UserRepository userRepository; - - public void regenerateRecommendations(Long userId) { - User user = userRepository.getReferenceById(userId); - int generatedCount = recommendationService.generateRecommendationsForUser(user); - log.info("사용자 {} 추천 즉시 재생성 완료: {} 개", userId, generatedCount); - } -} +package com.techfork.domain.recommendation.service; + +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class RecommendationCommandService { + + private final RecommendationService recommendationService; + private final UserRepository userRepository; + + public void regenerateRecommendations(Long userId) { + User user = userRepository.getReferenceById(userId); + int generatedCount = recommendationService.generateRecommendationsForUser(user); + log.info("사용자 {} 추천 즉시 재생성 완료: {} 개", userId, generatedCount); + } +} diff --git a/src/main/java/com/techfork/domain/recommendation/service/RecommendationQueryService.java b/src/main/java/com/techfork/domain/recommendation/service/RecommendationQueryService.java index e583a1c4..1e36db57 100644 --- a/src/main/java/com/techfork/domain/recommendation/service/RecommendationQueryService.java +++ b/src/main/java/com/techfork/domain/recommendation/service/RecommendationQueryService.java @@ -1,59 +1,59 @@ -package com.techfork.domain.recommendation.service; - -import com.techfork.domain.activity.repository.BookmarkRepository; -import com.techfork.domain.recommendation.converter.RecommendationConverter; -import com.techfork.domain.recommendation.dto.RecommendationListResponse; -import com.techfork.domain.recommendation.dto.RecommendedPostDto; -import com.techfork.domain.recommendation.entity.RecommendedPost; -import com.techfork.domain.recommendation.repository.RecommendedPostRepository; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Slf4j -@Service -@Transactional(readOnly = true) -@RequiredArgsConstructor -public class RecommendationQueryService { - - private final RecommendedPostRepository recommendedPostRepository; - private final UserRepository userRepository; - private final RecommendationConverter recommendationConverter; - private final BookmarkRepository bookmarkRepository; - - public RecommendationListResponse getRecommendations(Long userId) { - User user = userRepository.getReferenceById(userId); - List recommendedPosts = recommendedPostRepository.findByUserOrderByRankAsc(user); - log.info("사용자 {} 추천 목록 조회: {} 개", userId, recommendedPosts.size()); - - RecommendationListResponse response = recommendationConverter.toRecommendationListResponse(recommendedPosts); - response = attachBookmarkStatus(response, userId); - - return response; - } - - private RecommendationListResponse attachBookmarkStatus(RecommendationListResponse response, Long userId) { - if (response.recommendations().isEmpty()) { - return response; - } - - List postIds = response.recommendations().stream() - .map(RecommendedPostDto::postId) - .toList(); - List bookmarkedPostIds = bookmarkRepository.findBookmarkedPostIds(userId, postIds); - - List updatedRecommendations = response.recommendations().stream() - .map(dto -> dto.withBookmarkStatus(bookmarkedPostIds.contains(dto.postId()))) - .toList(); - - return RecommendationListResponse.builder() - .recommendations(updatedRecommendations) - .totalCount(response.totalCount()) - .build(); - } -} +package com.techfork.domain.recommendation.service; + +import com.techfork.domain.activity.repository.BookmarkRepository; +import com.techfork.domain.recommendation.converter.RecommendationConverter; +import com.techfork.domain.recommendation.dto.RecommendationListResponse; +import com.techfork.domain.recommendation.dto.RecommendedPostDto; +import com.techfork.domain.recommendation.entity.RecommendedPost; +import com.techfork.domain.recommendation.repository.RecommendedPostRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class RecommendationQueryService { + + private final RecommendedPostRepository recommendedPostRepository; + private final UserRepository userRepository; + private final RecommendationConverter recommendationConverter; + private final BookmarkRepository bookmarkRepository; + + public RecommendationListResponse getRecommendations(Long userId) { + User user = userRepository.getReferenceById(userId); + List recommendedPosts = recommendedPostRepository.findByUserOrderByRankAsc(user); + log.info("사용자 {} 추천 목록 조회: {} 개", userId, recommendedPosts.size()); + + RecommendationListResponse response = recommendationConverter.toRecommendationListResponse(recommendedPosts); + response = attachBookmarkStatus(response, userId); + + return response; + } + + private RecommendationListResponse attachBookmarkStatus(RecommendationListResponse response, Long userId) { + if (response.recommendations().isEmpty()) { + return response; + } + + List postIds = response.recommendations().stream() + .map(RecommendedPostDto::postId) + .toList(); + List bookmarkedPostIds = bookmarkRepository.findBookmarkedPostIds(userId, postIds); + + List updatedRecommendations = response.recommendations().stream() + .map(dto -> dto.withBookmarkStatus(bookmarkedPostIds.contains(dto.postId()))) + .toList(); + + return RecommendationListResponse.builder() + .recommendations(updatedRecommendations) + .totalCount(response.totalCount()) + .build(); + } +} diff --git a/src/main/java/com/techfork/domain/recommendation/service/RecommendationService.java b/src/main/java/com/techfork/domain/recommendation/service/RecommendationService.java index a6b2cfac..ebb5fb40 100644 --- a/src/main/java/com/techfork/domain/recommendation/service/RecommendationService.java +++ b/src/main/java/com/techfork/domain/recommendation/service/RecommendationService.java @@ -1,18 +1,18 @@ -package com.techfork.domain.recommendation.service; - -import com.techfork.domain.user.entity.User; - -/** - * 추천 전략 인터페이스 - * 다양한 추천 알고리즘을 구현할 수 있도록 추상화 - */ -public interface RecommendationService { - - /** - * 사용자별 개인화 추천 게시글 생성 - * - * @param user 추천 대상 사용자 - * @return 생성된 추천 개수 - */ - int generateRecommendationsForUser(User user); -} +package com.techfork.domain.recommendation.service; + +import com.techfork.domain.useraccount.entity.User; + +/** + * 추천 전략 인터페이스 + * 다양한 추천 알고리즘을 구현할 수 있도록 추상화 + */ +public interface RecommendationService { + + /** + * 사용자별 개인화 추천 게시글 생성 + * + * @param user 추천 대상 사용자 + * @return 생성된 추천 개수 + */ + int generateRecommendationsForUser(User user); +} diff --git a/src/main/java/com/techfork/domain/search/service/SearchServiceImpl.java b/src/main/java/com/techfork/domain/search/service/SearchServiceImpl.java index e7da61cd..5f528519 100644 --- a/src/main/java/com/techfork/domain/search/service/SearchServiceImpl.java +++ b/src/main/java/com/techfork/domain/search/service/SearchServiceImpl.java @@ -13,8 +13,8 @@ import com.techfork.domain.search.config.GeneralSearchProperties; import com.techfork.domain.search.dto.SearchResult; -import com.techfork.domain.user.document.PersonalizationProfileDocument; -import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; +import com.techfork.domain.personalization.document.PersonalizationProfileDocument; +import com.techfork.domain.personalization.repository.PersonalizationProfileDocumentRepository; import com.techfork.global.llm.EmbeddingClient; import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; import com.techfork.global.util.RrfScorer; diff --git a/src/main/java/com/techfork/domain/user/controller/OnboardingController.java b/src/main/java/com/techfork/domain/useraccount/controller/OnboardingController.java similarity index 85% rename from src/main/java/com/techfork/domain/user/controller/OnboardingController.java rename to src/main/java/com/techfork/domain/useraccount/controller/OnboardingController.java index 38bcd2e7..a201d547 100644 --- a/src/main/java/com/techfork/domain/user/controller/OnboardingController.java +++ b/src/main/java/com/techfork/domain/useraccount/controller/OnboardingController.java @@ -1,51 +1,51 @@ -package com.techfork.domain.user.controller; - -import com.techfork.domain.user.dto.InterestListResponse; -import com.techfork.domain.user.dto.OnboardingRequest; -import com.techfork.domain.user.service.InterestQueryService; -import com.techfork.domain.user.service.UserCommandService; -import com.techfork.global.common.code.SuccessCode; -import com.techfork.global.response.BaseResponse; -import com.techfork.global.security.oauth.UserPrincipal; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; - -@Tag(name = "Onboarding", description = "온보딩 API") -@Slf4j -@RestController -@RequestMapping("/api/v1/onboarding") -@RequiredArgsConstructor -public class OnboardingController { - - private final InterestQueryService interestQueryService; - private final UserCommandService userCommandService; - - @Operation( - summary = "관심사 목록 조회", - description = "온보딩 시 선택 가능한 모든 관심사 카테고리와 키워드 목록을 조회합니다." - ) - @GetMapping("/interests") - public ResponseEntity> getInterests() { - InterestListResponse response = interestQueryService.getAllInterests(); - return BaseResponse.of(SuccessCode.OK, response); - } - - @Operation( - summary = "내 정보 및 관심사 저장", - description = "온보딩 시 사용자의 정보와 선택한 관심사를 저장합니다. 카테고리별로 세부 키워드를 선택할 수 있습니다." - ) - @PostMapping("/complete") - public ResponseEntity> completeOnboarding( - @AuthenticationPrincipal UserPrincipal userPrincipal, - @Valid @RequestBody OnboardingRequest request - ) { - userCommandService.completeOnboarding(userPrincipal.getId(), request); - return BaseResponse.of(SuccessCode.CREATED); - } -} +package com.techfork.domain.useraccount.controller; + +import com.techfork.domain.useraccount.dto.InterestListResponse; +import com.techfork.domain.useraccount.dto.OnboardingRequest; +import com.techfork.domain.useraccount.service.InterestQueryService; +import com.techfork.domain.useraccount.service.UserCommandService; +import com.techfork.global.common.code.SuccessCode; +import com.techfork.global.response.BaseResponse; +import com.techfork.global.security.oauth.UserPrincipal; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "Onboarding", description = "온보딩 API") +@Slf4j +@RestController +@RequestMapping("/api/v1/onboarding") +@RequiredArgsConstructor +public class OnboardingController { + + private final InterestQueryService interestQueryService; + private final UserCommandService userCommandService; + + @Operation( + summary = "관심사 목록 조회", + description = "온보딩 시 선택 가능한 모든 관심사 카테고리와 키워드 목록을 조회합니다." + ) + @GetMapping("/interests") + public ResponseEntity> getInterests() { + InterestListResponse response = interestQueryService.getAllInterests(); + return BaseResponse.of(SuccessCode.OK, response); + } + + @Operation( + summary = "내 정보 및 관심사 저장", + description = "온보딩 시 사용자의 정보와 선택한 관심사를 저장합니다. 카테고리별로 세부 키워드를 선택할 수 있습니다." + ) + @PostMapping("/complete") + public ResponseEntity> completeOnboarding( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @Valid @RequestBody OnboardingRequest request + ) { + userCommandService.completeOnboarding(userPrincipal.getId(), request); + return BaseResponse.of(SuccessCode.CREATED); + } +} diff --git a/src/main/java/com/techfork/domain/user/controller/UserController.java b/src/main/java/com/techfork/domain/useraccount/controller/UserController.java similarity index 87% rename from src/main/java/com/techfork/domain/user/controller/UserController.java rename to src/main/java/com/techfork/domain/useraccount/controller/UserController.java index 2243c11e..83e27649 100644 --- a/src/main/java/com/techfork/domain/user/controller/UserController.java +++ b/src/main/java/com/techfork/domain/useraccount/controller/UserController.java @@ -1,60 +1,60 @@ -package com.techfork.domain.user.controller; +package com.techfork.domain.useraccount.controller; -import com.techfork.domain.user.dto.SaveInterestRequest; -import com.techfork.domain.user.dto.UpdateAccountProfileRequest; -import com.techfork.domain.user.dto.UserInterestResponse; -import com.techfork.domain.user.dto.AccountProfileResponse; -import com.techfork.domain.user.service.InterestCommandService; -import com.techfork.domain.user.service.InterestQueryService; -import com.techfork.domain.user.service.UserCommandService; -import com.techfork.domain.user.service.UserQueryService; -import com.techfork.global.common.code.SuccessCode; -import com.techfork.global.response.BaseResponse; -import com.techfork.global.security.oauth.UserPrincipal; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; - -@Tag(name = "User", description = "사용자 API") -@Slf4j -@RestController -@RequestMapping("/api/v1/users") -@RequiredArgsConstructor -public class UserController { - - private final InterestCommandService interestCommandService; - private final InterestQueryService interestQueryService; - private final UserCommandService userCommandService; - private final UserQueryService userQueryService; - - @Operation( - summary = "내 관심사 수정", - description = "현재 로그인한 사용자의 관심사를 수정합니다. 기존 관심사는 모두 삭제되고 새로운 관심사로 대체됩니다." - ) - @PutMapping("/me/interests") - public ResponseEntity> updateMyInterests( - @AuthenticationPrincipal UserPrincipal userPrincipal, - @Valid @RequestBody SaveInterestRequest request - ) { - interestCommandService.updateUserInterests(userPrincipal.getId(), request); - return BaseResponse.of(SuccessCode.OK); - } - - @Operation( - summary = "내 관심사 조회", - description = "현재 로그인한 사용자의 관심사 목록을 조회합니다." - ) - @GetMapping("/me/interests") - public ResponseEntity> getMyInterests( - @AuthenticationPrincipal UserPrincipal userPrincipal - ) { - UserInterestResponse response = interestQueryService.getUserInterests(userPrincipal.getId()); - return BaseResponse.of(SuccessCode.OK, response); +import com.techfork.domain.useraccount.dto.SaveInterestRequest; +import com.techfork.domain.useraccount.dto.UpdateAccountProfileRequest; +import com.techfork.domain.useraccount.dto.UserInterestResponse; +import com.techfork.domain.useraccount.dto.AccountProfileResponse; +import com.techfork.domain.useraccount.service.InterestCommandService; +import com.techfork.domain.useraccount.service.InterestQueryService; +import com.techfork.domain.useraccount.service.UserCommandService; +import com.techfork.domain.useraccount.service.UserQueryService; +import com.techfork.global.common.code.SuccessCode; +import com.techfork.global.response.BaseResponse; +import com.techfork.global.security.oauth.UserPrincipal; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "User", description = "사용자 API") +@Slf4j +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final InterestCommandService interestCommandService; + private final InterestQueryService interestQueryService; + private final UserCommandService userCommandService; + private final UserQueryService userQueryService; + + @Operation( + summary = "내 관심사 수정", + description = "현재 로그인한 사용자의 관심사를 수정합니다. 기존 관심사는 모두 삭제되고 새로운 관심사로 대체됩니다." + ) + @PutMapping("/me/interests") + public ResponseEntity> updateMyInterests( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @Valid @RequestBody SaveInterestRequest request + ) { + interestCommandService.updateUserInterests(userPrincipal.getId(), request); + return BaseResponse.of(SuccessCode.OK); + } + + @Operation( + summary = "내 관심사 조회", + description = "현재 로그인한 사용자의 관심사 목록을 조회합니다." + ) + @GetMapping("/me/interests") + public ResponseEntity> getMyInterests( + @AuthenticationPrincipal UserPrincipal userPrincipal + ) { + UserInterestResponse response = interestQueryService.getUserInterests(userPrincipal.getId()); + return BaseResponse.of(SuccessCode.OK, response); } @Operation( @@ -81,16 +81,16 @@ public ResponseEntity> getMyAccountProfile( AccountProfileResponse response = userQueryService.getAccountProfile(userPrincipal.getId()); return BaseResponse.of(SuccessCode.OK, response); } - + @Operation( summary = "회원 탈퇴", description = "현재 로그인한 사용자의 계정을 탈퇴 처리합니다. 계정 프로필 개인정보는 즉시 익명화되며, 활동 기록은 통계 목적으로 유지됩니다. 탈퇴 후 동일한 소셜 계정으로 재가입 시 새로운 회원으로 온보딩부터 다시 시작합니다." ) - @PatchMapping("/me/withdrawal") - public ResponseEntity> withdrawUser( - @AuthenticationPrincipal UserPrincipal userPrincipal - ) { - userCommandService.withdrawUser(userPrincipal.getId()); - return BaseResponse.of(SuccessCode.OK); - } -} + @PatchMapping("/me/withdrawal") + public ResponseEntity> withdrawUser( + @AuthenticationPrincipal UserPrincipal userPrincipal + ) { + userCommandService.withdrawUser(userPrincipal.getId()); + return BaseResponse.of(SuccessCode.OK); + } +} diff --git a/src/main/java/com/techfork/domain/user/converter/InterestConverter.java b/src/main/java/com/techfork/domain/useraccount/converter/InterestConverter.java similarity index 80% rename from src/main/java/com/techfork/domain/user/converter/InterestConverter.java rename to src/main/java/com/techfork/domain/useraccount/converter/InterestConverter.java index 535e471a..9f346eb6 100644 --- a/src/main/java/com/techfork/domain/user/converter/InterestConverter.java +++ b/src/main/java/com/techfork/domain/useraccount/converter/InterestConverter.java @@ -1,47 +1,47 @@ -package com.techfork.domain.user.converter; - -import com.techfork.domain.user.dto.InterestListResponse; -import com.techfork.domain.user.dto.UserInterestDto; -import com.techfork.domain.user.entity.UserInterestCategory; -import com.techfork.domain.user.enums.EInterestCategory; -import com.techfork.domain.user.enums.EInterestKeyword; -import org.springframework.stereotype.Component; - -import java.util.Arrays; -import java.util.List; - -@Component -public class InterestConverter { - - public List toInterestCategoryDtoList() { - return Arrays.stream(EInterestCategory.values()) - .map(category -> { - List keywords = EInterestKeyword.getKeywordsByCategory(category) - .stream() - .map(keyword -> new InterestListResponse.Keyword(keyword.name(), keyword.getDisplayName())) - .toList(); - - return InterestListResponse.Category.builder() - .category(category.name()) - .displayName(category.getDisplayName()) - .keywords(keywords) - .build(); - }) - .toList(); - } - - public List toUserInterestDtoList(List categories) { - return categories.stream() - .map(category -> { - List keywords = category.getKeywords().stream() - .map(keyword -> keyword.getKeyword().name()) - .toList(); - - return UserInterestDto.builder() - .category(category.getCategory().name()) - .keywords(keywords) - .build(); - }) - .toList(); - } -} +package com.techfork.domain.useraccount.converter; + +import com.techfork.domain.useraccount.dto.InterestListResponse; +import com.techfork.domain.useraccount.dto.UserInterestDto; +import com.techfork.domain.useraccount.entity.UserInterestCategory; +import com.techfork.domain.useraccount.enums.EInterestCategory; +import com.techfork.domain.useraccount.enums.EInterestKeyword; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.List; + +@Component +public class InterestConverter { + + public List toInterestCategoryDtoList() { + return Arrays.stream(EInterestCategory.values()) + .map(category -> { + List keywords = EInterestKeyword.getKeywordsByCategory(category) + .stream() + .map(keyword -> new InterestListResponse.Keyword(keyword.name(), keyword.getDisplayName())) + .toList(); + + return InterestListResponse.Category.builder() + .category(category.name()) + .displayName(category.getDisplayName()) + .keywords(keywords) + .build(); + }) + .toList(); + } + + public List toUserInterestDtoList(List categories) { + return categories.stream() + .map(category -> { + List keywords = category.getKeywords().stream() + .map(keyword -> keyword.getKeyword().name()) + .toList(); + + return UserInterestDto.builder() + .category(category.getCategory().name()) + .keywords(keywords) + .build(); + }) + .toList(); + } +} diff --git a/src/main/java/com/techfork/domain/user/converter/UserConverter.java b/src/main/java/com/techfork/domain/useraccount/converter/UserConverter.java similarity index 72% rename from src/main/java/com/techfork/domain/user/converter/UserConverter.java rename to src/main/java/com/techfork/domain/useraccount/converter/UserConverter.java index 026c5cd0..75d4f3ed 100644 --- a/src/main/java/com/techfork/domain/user/converter/UserConverter.java +++ b/src/main/java/com/techfork/domain/useraccount/converter/UserConverter.java @@ -1,7 +1,7 @@ -package com.techfork.domain.user.converter; +package com.techfork.domain.useraccount.converter; -import com.techfork.domain.user.dto.AccountProfileResponse; -import com.techfork.domain.user.entity.User; +import com.techfork.domain.useraccount.dto.AccountProfileResponse; +import com.techfork.domain.useraccount.entity.User; import org.springframework.stereotype.Component; @Component diff --git a/src/main/java/com/techfork/domain/user/dto/AccountProfileResponse.java b/src/main/java/com/techfork/domain/useraccount/dto/AccountProfileResponse.java similarity index 80% rename from src/main/java/com/techfork/domain/user/dto/AccountProfileResponse.java rename to src/main/java/com/techfork/domain/useraccount/dto/AccountProfileResponse.java index 53bdd252..6fd4e595 100644 --- a/src/main/java/com/techfork/domain/user/dto/AccountProfileResponse.java +++ b/src/main/java/com/techfork/domain/useraccount/dto/AccountProfileResponse.java @@ -1,4 +1,4 @@ -package com.techfork.domain.user.dto; +package com.techfork.domain.useraccount.dto; import lombok.Builder; diff --git a/src/main/java/com/techfork/domain/user/dto/InterestListResponse.java b/src/main/java/com/techfork/domain/useraccount/dto/InterestListResponse.java similarity index 86% rename from src/main/java/com/techfork/domain/user/dto/InterestListResponse.java rename to src/main/java/com/techfork/domain/useraccount/dto/InterestListResponse.java index 4e76c669..80dedb24 100644 --- a/src/main/java/com/techfork/domain/user/dto/InterestListResponse.java +++ b/src/main/java/com/techfork/domain/useraccount/dto/InterestListResponse.java @@ -1,22 +1,22 @@ -package com.techfork.domain.user.dto; - -import lombok.Builder; - -import java.util.List; - -@Builder -public record InterestListResponse( - List categories -) { - @Builder - public record Category( - String category, - String displayName, - List keywords - ) {} - - public record Keyword( - String keyword, - String displayName - ) {} -} +package com.techfork.domain.useraccount.dto; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record InterestListResponse( + List categories +) { + @Builder + public record Category( + String category, + String displayName, + List keywords + ) {} + + public record Keyword( + String keyword, + String displayName + ) {} +} diff --git a/src/main/java/com/techfork/domain/user/dto/OnboardingRequest.java b/src/main/java/com/techfork/domain/useraccount/dto/OnboardingRequest.java similarity index 94% rename from src/main/java/com/techfork/domain/user/dto/OnboardingRequest.java rename to src/main/java/com/techfork/domain/useraccount/dto/OnboardingRequest.java index 2fa753d2..24aadf55 100644 --- a/src/main/java/com/techfork/domain/user/dto/OnboardingRequest.java +++ b/src/main/java/com/techfork/domain/useraccount/dto/OnboardingRequest.java @@ -1,4 +1,4 @@ -package com.techfork.domain.user.dto; +package com.techfork.domain.useraccount.dto; import jakarta.validation.constraints.*; diff --git a/src/main/java/com/techfork/domain/user/dto/SaveInterestRequest.java b/src/main/java/com/techfork/domain/useraccount/dto/SaveInterestRequest.java similarity index 87% rename from src/main/java/com/techfork/domain/user/dto/SaveInterestRequest.java rename to src/main/java/com/techfork/domain/useraccount/dto/SaveInterestRequest.java index ffac4538..6555a57a 100644 --- a/src/main/java/com/techfork/domain/user/dto/SaveInterestRequest.java +++ b/src/main/java/com/techfork/domain/useraccount/dto/SaveInterestRequest.java @@ -1,13 +1,13 @@ -package com.techfork.domain.user.dto; - -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; - -import java.util.List; - -public record SaveInterestRequest( - @NotNull(message = "관심사 목록은 필수입니다.") - @NotEmpty(message = "관심사를 최소 1개 이상 선택해주세요.") - List interests -) { -} +package com.techfork.domain.useraccount.dto; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +public record SaveInterestRequest( + @NotNull(message = "관심사 목록은 필수입니다.") + @NotEmpty(message = "관심사를 최소 1개 이상 선택해주세요.") + List interests +) { +} diff --git a/src/main/java/com/techfork/domain/user/dto/UpdateAccountProfileRequest.java b/src/main/java/com/techfork/domain/useraccount/dto/UpdateAccountProfileRequest.java similarity index 89% rename from src/main/java/com/techfork/domain/user/dto/UpdateAccountProfileRequest.java rename to src/main/java/com/techfork/domain/useraccount/dto/UpdateAccountProfileRequest.java index d26b3544..bbcf4095 100644 --- a/src/main/java/com/techfork/domain/user/dto/UpdateAccountProfileRequest.java +++ b/src/main/java/com/techfork/domain/useraccount/dto/UpdateAccountProfileRequest.java @@ -1,4 +1,4 @@ -package com.techfork.domain.user.dto; +package com.techfork.domain.useraccount.dto; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/com/techfork/domain/user/dto/UserInterestDto.java b/src/main/java/com/techfork/domain/useraccount/dto/UserInterestDto.java similarity index 75% rename from src/main/java/com/techfork/domain/user/dto/UserInterestDto.java rename to src/main/java/com/techfork/domain/useraccount/dto/UserInterestDto.java index d72ea6f1..9ce4789c 100644 --- a/src/main/java/com/techfork/domain/user/dto/UserInterestDto.java +++ b/src/main/java/com/techfork/domain/useraccount/dto/UserInterestDto.java @@ -1,12 +1,12 @@ -package com.techfork.domain.user.dto; - -import lombok.Builder; - -import java.util.List; - -@Builder -public record UserInterestDto( - String category, - List keywords -) { -} +package com.techfork.domain.useraccount.dto; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record UserInterestDto( + String category, + List keywords +) { +} diff --git a/src/main/java/com/techfork/domain/user/dto/UserInterestResponse.java b/src/main/java/com/techfork/domain/useraccount/dto/UserInterestResponse.java similarity index 74% rename from src/main/java/com/techfork/domain/user/dto/UserInterestResponse.java rename to src/main/java/com/techfork/domain/useraccount/dto/UserInterestResponse.java index 42cc8a0c..c0d84082 100644 --- a/src/main/java/com/techfork/domain/user/dto/UserInterestResponse.java +++ b/src/main/java/com/techfork/domain/useraccount/dto/UserInterestResponse.java @@ -1,11 +1,11 @@ -package com.techfork.domain.user.dto; - -import lombok.Builder; - -import java.util.List; - -@Builder -public record UserInterestResponse( - List interests -) { -} +package com.techfork.domain.useraccount.dto; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record UserInterestResponse( + List interests +) { +} diff --git a/src/main/java/com/techfork/domain/user/entity/User.java b/src/main/java/com/techfork/domain/useraccount/entity/User.java similarity index 91% rename from src/main/java/com/techfork/domain/user/entity/User.java rename to src/main/java/com/techfork/domain/useraccount/entity/User.java index 0551be8b..086526fe 100644 --- a/src/main/java/com/techfork/domain/user/entity/User.java +++ b/src/main/java/com/techfork/domain/useraccount/entity/User.java @@ -1,111 +1,111 @@ -package com.techfork.domain.user.entity; - -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.enums.UserStatus; -import com.techfork.global.common.BaseTimeEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.PersistenceCreator; - -import java.util.ArrayList; -import java.util.List; - -@Entity -@Table(name = "users", uniqueConstraints = { - @UniqueConstraint(columnNames = {"social_type", "social_id"}) -}) -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class User extends BaseTimeEntity { - - private String nickName; - - private String email; - - private String profileImage; - - private String description; - - @Enumerated(EnumType.STRING) - @Column(name = "social_type", nullable = false) - private SocialType socialType; - - @Column(name = "social_id", nullable = false) - private String socialId; - - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private Role role; - - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private UserStatus status; - - @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) - private List interestCategories = new ArrayList<>(); - - @PersistenceCreator - @Builder - User(String nickName, String email, String profileImage, String description, SocialType socialType, String socialId, Role role, UserStatus status) { - this.nickName = nickName; - this.email = email; - this.profileImage = profileImage; - this.description = description; - this.socialType = socialType; - this.socialId = socialId; - this.role = role != null ? role : Role.USER; - this.status = status != null ? status : UserStatus.PENDING; - } - - public static User createSocialUser(SocialType socialType, String socialId, String email, String profileImage) { - return User.builder() - .socialType(socialType) - .socialId(socialId) - .email(email) - .profileImage(profileImage) - .role(Role.USER) - .build(); - } - - public void updateUser(String nickName, String email, String description) { - this.nickName = nickName; - this.email = email; - this.description = description; - this.status = UserStatus.ACTIVE; - } - - public void updateProfile(String nickName, String description) { - if (nickName != null) { - this.nickName = nickName; - } - if (description != null) { - this.description = description; - } - } - - public boolean isActive() { - return status == UserStatus.ACTIVE; - } - - public boolean isWithdrawn() { - return status == UserStatus.WITHDRAWN; - } - - public void withdraw() { - this.status = UserStatus.WITHDRAWN; - this.nickName = null; - this.email = null; - this.profileImage = null; - this.description = null; - } - - public void reactivate(String email, String profileImage) { - this.email = email; - this.profileImage = profileImage; - this.status = UserStatus.PENDING; - } -} +package com.techfork.domain.useraccount.entity; + +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.enums.UserStatus; +import com.techfork.global.common.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.PersistenceCreator; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "users", uniqueConstraints = { + @UniqueConstraint(columnNames = {"social_type", "social_id"}) +}) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class User extends BaseTimeEntity { + + private String nickName; + + private String email; + + private String profileImage; + + private String description; + + @Enumerated(EnumType.STRING) + @Column(name = "social_type", nullable = false) + private SocialType socialType; + + @Column(name = "social_id", nullable = false) + private String socialId; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private UserStatus status; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private List interestCategories = new ArrayList<>(); + + @PersistenceCreator + @Builder + User(String nickName, String email, String profileImage, String description, SocialType socialType, String socialId, Role role, UserStatus status) { + this.nickName = nickName; + this.email = email; + this.profileImage = profileImage; + this.description = description; + this.socialType = socialType; + this.socialId = socialId; + this.role = role != null ? role : Role.USER; + this.status = status != null ? status : UserStatus.PENDING; + } + + public static User createSocialUser(SocialType socialType, String socialId, String email, String profileImage) { + return User.builder() + .socialType(socialType) + .socialId(socialId) + .email(email) + .profileImage(profileImage) + .role(Role.USER) + .build(); + } + + public void updateUser(String nickName, String email, String description) { + this.nickName = nickName; + this.email = email; + this.description = description; + this.status = UserStatus.ACTIVE; + } + + public void updateProfile(String nickName, String description) { + if (nickName != null) { + this.nickName = nickName; + } + if (description != null) { + this.description = description; + } + } + + public boolean isActive() { + return status == UserStatus.ACTIVE; + } + + public boolean isWithdrawn() { + return status == UserStatus.WITHDRAWN; + } + + public void withdraw() { + this.status = UserStatus.WITHDRAWN; + this.nickName = null; + this.email = null; + this.profileImage = null; + this.description = null; + } + + public void reactivate(String email, String profileImage) { + this.email = email; + this.profileImage = profileImage; + this.status = UserStatus.PENDING; + } +} diff --git a/src/main/java/com/techfork/domain/user/entity/UserInterestCategory.java b/src/main/java/com/techfork/domain/useraccount/entity/UserInterestCategory.java similarity index 90% rename from src/main/java/com/techfork/domain/user/entity/UserInterestCategory.java rename to src/main/java/com/techfork/domain/useraccount/entity/UserInterestCategory.java index aa34f2ae..bf7b08bc 100644 --- a/src/main/java/com/techfork/domain/user/entity/UserInterestCategory.java +++ b/src/main/java/com/techfork/domain/useraccount/entity/UserInterestCategory.java @@ -1,50 +1,50 @@ -package com.techfork.domain.user.entity; - -import com.techfork.domain.user.enums.EInterestCategory; -import com.techfork.global.common.BaseEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.PersistenceCreator; - -import java.util.ArrayList; -import java.util.List; - -@Entity -@Table(name = "user_interest_categories") -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class UserInterestCategory extends BaseEntity { - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; - - @Enumerated(EnumType.STRING) - @Column(nullable = false, length = 50) - private EInterestCategory category; - - @OneToMany(mappedBy = "userInterestCategory", cascade = CascadeType.ALL, orphanRemoval = true) - private List keywords = new ArrayList<>(); - - @PersistenceCreator - @Builder - UserInterestCategory(User user, EInterestCategory category) { - this.user = user; - this.category = category; - } - - public static UserInterestCategory create(User user, EInterestCategory category) { - return UserInterestCategory.builder() - .user(user) - .category(category) - .build(); - } - - public void addKeyword(UserInterestKeyword keyword) { - this.keywords.add(keyword); - keyword.setUserInterestCategory(this); - } -} +package com.techfork.domain.useraccount.entity; + +import com.techfork.domain.useraccount.enums.EInterestCategory; +import com.techfork.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.PersistenceCreator; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "user_interest_categories") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UserInterestCategory extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 50) + private EInterestCategory category; + + @OneToMany(mappedBy = "userInterestCategory", cascade = CascadeType.ALL, orphanRemoval = true) + private List keywords = new ArrayList<>(); + + @PersistenceCreator + @Builder + UserInterestCategory(User user, EInterestCategory category) { + this.user = user; + this.category = category; + } + + public static UserInterestCategory create(User user, EInterestCategory category) { + return UserInterestCategory.builder() + .user(user) + .category(category) + .build(); + } + + public void addKeyword(UserInterestKeyword keyword) { + this.keywords.add(keyword); + keyword.setUserInterestCategory(this); + } +} diff --git a/src/main/java/com/techfork/domain/user/entity/UserInterestKeyword.java b/src/main/java/com/techfork/domain/useraccount/entity/UserInterestKeyword.java similarity index 90% rename from src/main/java/com/techfork/domain/user/entity/UserInterestKeyword.java rename to src/main/java/com/techfork/domain/useraccount/entity/UserInterestKeyword.java index fd6bc607..3274e31a 100644 --- a/src/main/java/com/techfork/domain/user/entity/UserInterestKeyword.java +++ b/src/main/java/com/techfork/domain/useraccount/entity/UserInterestKeyword.java @@ -1,43 +1,43 @@ -package com.techfork.domain.user.entity; - -import com.techfork.domain.user.enums.EInterestKeyword; -import com.techfork.global.common.BaseEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.PersistenceCreator; - -@Entity -@Table(name = "user_interest_keywords") -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class UserInterestKeyword extends BaseEntity { - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_interest_category_id", nullable = false) - private UserInterestCategory userInterestCategory; - - @Enumerated(EnumType.STRING) - @Column(nullable = false, length = 50) - private EInterestKeyword keyword; - - @PersistenceCreator - @Builder - UserInterestKeyword(UserInterestCategory userInterestCategory, EInterestKeyword keyword) { - this.userInterestCategory = userInterestCategory; - this.keyword = keyword; - } - - public static UserInterestKeyword create(UserInterestCategory userInterestCategory, EInterestKeyword keyword) { - return UserInterestKeyword.builder() - .userInterestCategory(userInterestCategory) - .keyword(keyword) - .build(); - } - - protected void setUserInterestCategory(UserInterestCategory userInterestCategory) { - this.userInterestCategory = userInterestCategory; - } -} +package com.techfork.domain.useraccount.entity; + +import com.techfork.domain.useraccount.enums.EInterestKeyword; +import com.techfork.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.PersistenceCreator; + +@Entity +@Table(name = "user_interest_keywords") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UserInterestKeyword extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_interest_category_id", nullable = false) + private UserInterestCategory userInterestCategory; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 50) + private EInterestKeyword keyword; + + @PersistenceCreator + @Builder + UserInterestKeyword(UserInterestCategory userInterestCategory, EInterestKeyword keyword) { + this.userInterestCategory = userInterestCategory; + this.keyword = keyword; + } + + public static UserInterestKeyword create(UserInterestCategory userInterestCategory, EInterestKeyword keyword) { + return UserInterestKeyword.builder() + .userInterestCategory(userInterestCategory) + .keyword(keyword) + .build(); + } + + protected void setUserInterestCategory(UserInterestCategory userInterestCategory) { + this.userInterestCategory = userInterestCategory; + } +} diff --git a/src/main/java/com/techfork/domain/user/enums/EInterestCategory.java b/src/main/java/com/techfork/domain/useraccount/enums/EInterestCategory.java similarity index 89% rename from src/main/java/com/techfork/domain/user/enums/EInterestCategory.java rename to src/main/java/com/techfork/domain/useraccount/enums/EInterestCategory.java index 23dea0a1..80d82454 100644 --- a/src/main/java/com/techfork/domain/user/enums/EInterestCategory.java +++ b/src/main/java/com/techfork/domain/useraccount/enums/EInterestCategory.java @@ -1,44 +1,44 @@ -package com.techfork.domain.user.enums; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum EInterestCategory { - - IOS("iOS"), - ANDROID("Android"), - - FRONTEND("Frontend"), - - BACKEND("Backend"), - - DATA_ENGINEERING("Data Engineering"), - DATA_SCIENCE("Data Science"), - DATABASE("Database"), - - AI_ML("AI/ML"), - - DEVOPS("DevOps"), - CLOUD("Cloud"), - SYSTEMS_OS("Systems/OS"), - NETWORKING("Networking"), - - SECURITY("Security"), - - GAME_DEV("Game Dev"), - AR_VR_XR("AR/VR/XR"), - - EMBEDDED_IOT("Embedded/IoT"), - - BLOCKCHAIN_WEB3("Blockchain/Web3"), - - QA_TEST("QA/Test"), - - PRODUCT_UX("Product/UX"), - - ARCHITECTURE("Architecture"); - - private final String displayName; -} +package com.techfork.domain.useraccount.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum EInterestCategory { + + IOS("iOS"), + ANDROID("Android"), + + FRONTEND("Frontend"), + + BACKEND("Backend"), + + DATA_ENGINEERING("Data Engineering"), + DATA_SCIENCE("Data Science"), + DATABASE("Database"), + + AI_ML("AI/ML"), + + DEVOPS("DevOps"), + CLOUD("Cloud"), + SYSTEMS_OS("Systems/OS"), + NETWORKING("Networking"), + + SECURITY("Security"), + + GAME_DEV("Game Dev"), + AR_VR_XR("AR/VR/XR"), + + EMBEDDED_IOT("Embedded/IoT"), + + BLOCKCHAIN_WEB3("Blockchain/Web3"), + + QA_TEST("QA/Test"), + + PRODUCT_UX("Product/UX"), + + ARCHITECTURE("Architecture"); + + private final String displayName; +} diff --git a/src/main/java/com/techfork/domain/user/enums/EInterestKeyword.java b/src/main/java/com/techfork/domain/useraccount/enums/EInterestKeyword.java similarity index 96% rename from src/main/java/com/techfork/domain/user/enums/EInterestKeyword.java rename to src/main/java/com/techfork/domain/useraccount/enums/EInterestKeyword.java index eaa8cdaf..65a92ccf 100644 --- a/src/main/java/com/techfork/domain/user/enums/EInterestKeyword.java +++ b/src/main/java/com/techfork/domain/useraccount/enums/EInterestKeyword.java @@ -1,152 +1,152 @@ -package com.techfork.domain.user.enums; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -@Getter -@RequiredArgsConstructor -public enum EInterestKeyword { - - // iOS - SWIFT(EInterestCategory.IOS, "Swift"), - SWIFTUI(EInterestCategory.IOS, "SwiftUI"), - UIKIT(EInterestCategory.IOS, "UIKit"), - XCODE(EInterestCategory.IOS, "Xcode"), - - // Android - KOTLIN(EInterestCategory.ANDROID, "Kotlin"), - ANDROID_JAVA(EInterestCategory.ANDROID, "Java"), - JETPACK_COMPOSE(EInterestCategory.ANDROID, "Jetpack Compose"), - ANDROID_STUDIO(EInterestCategory.ANDROID, "Android Studio"), - - // Frontend - REACT(EInterestCategory.FRONTEND, "React"), - VUE_JS(EInterestCategory.FRONTEND, "Vue.js"), - ANGULAR(EInterestCategory.FRONTEND, "Angular"), - JAVASCRIPT(EInterestCategory.FRONTEND, "JavaScript"), - TYPESCRIPT(EInterestCategory.FRONTEND, "TypeScript"), - - // Backend - JAVA(EInterestCategory.BACKEND, "Java"), - SPRING(EInterestCategory.BACKEND, "Spring"), - NODE_JS(EInterestCategory.BACKEND, "Node.js"), - PYTHON(EInterestCategory.BACKEND, "Python"), - DJANGO(EInterestCategory.BACKEND, "Django"), - - // Data Engineering - APACHE_SPARK(EInterestCategory.DATA_ENGINEERING, "Apache Spark"), - APACHE_KAFKA(EInterestCategory.DATA_ENGINEERING, "Apache Kafka"), - AIRFLOW(EInterestCategory.DATA_ENGINEERING, "Airflow"), - ETL(EInterestCategory.DATA_ENGINEERING, "ETL"), - - // Data Science - DS_PYTHON(EInterestCategory.DATA_SCIENCE, "Python"), - PANDAS(EInterestCategory.DATA_SCIENCE, "Pandas"), - NUMPY(EInterestCategory.DATA_SCIENCE, "NumPy"), - JUPYTER(EInterestCategory.DATA_SCIENCE, "Jupyter"), - SQL(EInterestCategory.DATA_SCIENCE, "SQL"), - - // Database - MYSQL(EInterestCategory.DATABASE, "MySQL"), - POSTGRESQL(EInterestCategory.DATABASE, "PostgreSQL"), - MONGODB(EInterestCategory.DATABASE, "MongoDB"), - REDIS(EInterestCategory.DATABASE, "Redis"), - ORACLE(EInterestCategory.DATABASE, "Oracle"), - - // AI/ML - TENSORFLOW(EInterestCategory.AI_ML, "TensorFlow"), - PYTORCH(EInterestCategory.AI_ML, "PyTorch"), - MACHINE_LEARNING(EInterestCategory.AI_ML, "Machine Learning"), - DEEP_LEARNING(EInterestCategory.AI_ML, "Deep Learning"), - - // DevOps - DOCKER(EInterestCategory.DEVOPS, "Docker"), - KUBERNETES(EInterestCategory.DEVOPS, "Kubernetes"), - DEVOPS_AWS(EInterestCategory.DEVOPS, "AWS"), - CI_CD(EInterestCategory.DEVOPS, "CI/CD"), - JENKINS(EInterestCategory.DEVOPS, "Jenkins"), - - // Cloud - AWS(EInterestCategory.CLOUD, "AWS"), - AZURE(EInterestCategory.CLOUD, "Azure"), - GCP(EInterestCategory.CLOUD, "GCP"), - FIREBASE(EInterestCategory.CLOUD, "Firebase"), - - // Systems/OS - LINUX(EInterestCategory.SYSTEMS_OS, "Linux"), - UNIX(EInterestCategory.SYSTEMS_OS, "Unix"), - WINDOWS_SERVER(EInterestCategory.SYSTEMS_OS, "Windows Server"), - SYSTEM_PROGRAMMING(EInterestCategory.SYSTEMS_OS, "시스템 프로그래밍"), - - // Networking - TCP_IP(EInterestCategory.NETWORKING, "TCP/IP"), - HTTP_HTTPS(EInterestCategory.NETWORKING, "HTTP/HTTPS"), - RESTFUL_API(EInterestCategory.NETWORKING, "RESTful API"), - WEBSOCKET(EInterestCategory.NETWORKING, "WebSocket"), - - // Security - NETWORK_SECURITY(EInterestCategory.SECURITY, "네트워크 보안"), - WEB_SECURITY(EInterestCategory.SECURITY, "웹 보안"), - ENCRYPTION(EInterestCategory.SECURITY, "암호화"), - AUTHENTICATION(EInterestCategory.SECURITY, "인증"), - - // Game Dev - UNITY(EInterestCategory.GAME_DEV, "Unity"), - UNREAL_ENGINE(EInterestCategory.GAME_DEV, "Unreal Engine"), - GAME_CSHARP(EInterestCategory.GAME_DEV, "C#"), - GAME_CPP(EInterestCategory.GAME_DEV, "C++"), - - // AR/VR/XR - ARKIT(EInterestCategory.AR_VR_XR, "ARKit"), - REALITYKIT(EInterestCategory.AR_VR_XR, "RealityKit"), - UNITY_AR(EInterestCategory.AR_VR_XR, "Unity AR"), - VR_DEVELOPMENT(EInterestCategory.AR_VR_XR, "VR Development"), - - // Embedded/IoT - C(EInterestCategory.EMBEDDED_IOT, "C"), - CPP(EInterestCategory.EMBEDDED_IOT, "C++"), - ARDUINO(EInterestCategory.EMBEDDED_IOT, "Arduino"), - RASPBERRY_PI(EInterestCategory.EMBEDDED_IOT, "Raspberry Pi"), - RTOS(EInterestCategory.EMBEDDED_IOT, "RTOS"), - - // Blockchain/Web3 - ETHEREUM(EInterestCategory.BLOCKCHAIN_WEB3, "이더리움"), - SMART_CONTRACT(EInterestCategory.BLOCKCHAIN_WEB3, "스마트 컨트랙트"), - SOLIDITY(EInterestCategory.BLOCKCHAIN_WEB3, "Solidity"), - WEB3(EInterestCategory.BLOCKCHAIN_WEB3, "Web3"), - BLOCKCHAIN_BASICS(EInterestCategory.BLOCKCHAIN_WEB3, "블록체인 기초"), - DAPP(EInterestCategory.BLOCKCHAIN_WEB3, "DApp"), - NFT(EInterestCategory.BLOCKCHAIN_WEB3, "NFT"), - CRYPTOCURRENCY(EInterestCategory.BLOCKCHAIN_WEB3, "암호화폐"), - - // QA/Test - JUNIT(EInterestCategory.QA_TEST, "JUnit"), - SELENIUM(EInterestCategory.QA_TEST, "Selenium"), - TEST_AUTOMATION(EInterestCategory.QA_TEST, "Test Automation"), - TDD(EInterestCategory.QA_TEST, "TDD"), - - // Product/UX - FIGMA(EInterestCategory.PRODUCT_UX, "Figma"), - SKETCH(EInterestCategory.PRODUCT_UX, "Sketch"), - ADOBE_XD(EInterestCategory.PRODUCT_UX, "Adobe XD"), - PROTOTYPING(EInterestCategory.PRODUCT_UX, "프로토타이핑"), - - // Architecture - MICROSERVICES(EInterestCategory.ARCHITECTURE, "Microservices"), - DDD(EInterestCategory.ARCHITECTURE, "DDD"), - DESIGN_PATTERNS(EInterestCategory.ARCHITECTURE, "Design Patterns"), - CLEAN_ARCHITECTURE(EInterestCategory.ARCHITECTURE, "Clean Architecture"); - - private final EInterestCategory category; - private final String displayName; - - public static List getKeywordsByCategory(EInterestCategory category) { - return Arrays.stream(values()) - .filter(keyword -> keyword.category == category) - .collect(Collectors.toList()); - } -} +package com.techfork.domain.useraccount.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@RequiredArgsConstructor +public enum EInterestKeyword { + + // iOS + SWIFT(EInterestCategory.IOS, "Swift"), + SWIFTUI(EInterestCategory.IOS, "SwiftUI"), + UIKIT(EInterestCategory.IOS, "UIKit"), + XCODE(EInterestCategory.IOS, "Xcode"), + + // Android + KOTLIN(EInterestCategory.ANDROID, "Kotlin"), + ANDROID_JAVA(EInterestCategory.ANDROID, "Java"), + JETPACK_COMPOSE(EInterestCategory.ANDROID, "Jetpack Compose"), + ANDROID_STUDIO(EInterestCategory.ANDROID, "Android Studio"), + + // Frontend + REACT(EInterestCategory.FRONTEND, "React"), + VUE_JS(EInterestCategory.FRONTEND, "Vue.js"), + ANGULAR(EInterestCategory.FRONTEND, "Angular"), + JAVASCRIPT(EInterestCategory.FRONTEND, "JavaScript"), + TYPESCRIPT(EInterestCategory.FRONTEND, "TypeScript"), + + // Backend + JAVA(EInterestCategory.BACKEND, "Java"), + SPRING(EInterestCategory.BACKEND, "Spring"), + NODE_JS(EInterestCategory.BACKEND, "Node.js"), + PYTHON(EInterestCategory.BACKEND, "Python"), + DJANGO(EInterestCategory.BACKEND, "Django"), + + // Data Engineering + APACHE_SPARK(EInterestCategory.DATA_ENGINEERING, "Apache Spark"), + APACHE_KAFKA(EInterestCategory.DATA_ENGINEERING, "Apache Kafka"), + AIRFLOW(EInterestCategory.DATA_ENGINEERING, "Airflow"), + ETL(EInterestCategory.DATA_ENGINEERING, "ETL"), + + // Data Science + DS_PYTHON(EInterestCategory.DATA_SCIENCE, "Python"), + PANDAS(EInterestCategory.DATA_SCIENCE, "Pandas"), + NUMPY(EInterestCategory.DATA_SCIENCE, "NumPy"), + JUPYTER(EInterestCategory.DATA_SCIENCE, "Jupyter"), + SQL(EInterestCategory.DATA_SCIENCE, "SQL"), + + // Database + MYSQL(EInterestCategory.DATABASE, "MySQL"), + POSTGRESQL(EInterestCategory.DATABASE, "PostgreSQL"), + MONGODB(EInterestCategory.DATABASE, "MongoDB"), + REDIS(EInterestCategory.DATABASE, "Redis"), + ORACLE(EInterestCategory.DATABASE, "Oracle"), + + // AI/ML + TENSORFLOW(EInterestCategory.AI_ML, "TensorFlow"), + PYTORCH(EInterestCategory.AI_ML, "PyTorch"), + MACHINE_LEARNING(EInterestCategory.AI_ML, "Machine Learning"), + DEEP_LEARNING(EInterestCategory.AI_ML, "Deep Learning"), + + // DevOps + DOCKER(EInterestCategory.DEVOPS, "Docker"), + KUBERNETES(EInterestCategory.DEVOPS, "Kubernetes"), + DEVOPS_AWS(EInterestCategory.DEVOPS, "AWS"), + CI_CD(EInterestCategory.DEVOPS, "CI/CD"), + JENKINS(EInterestCategory.DEVOPS, "Jenkins"), + + // Cloud + AWS(EInterestCategory.CLOUD, "AWS"), + AZURE(EInterestCategory.CLOUD, "Azure"), + GCP(EInterestCategory.CLOUD, "GCP"), + FIREBASE(EInterestCategory.CLOUD, "Firebase"), + + // Systems/OS + LINUX(EInterestCategory.SYSTEMS_OS, "Linux"), + UNIX(EInterestCategory.SYSTEMS_OS, "Unix"), + WINDOWS_SERVER(EInterestCategory.SYSTEMS_OS, "Windows Server"), + SYSTEM_PROGRAMMING(EInterestCategory.SYSTEMS_OS, "시스템 프로그래밍"), + + // Networking + TCP_IP(EInterestCategory.NETWORKING, "TCP/IP"), + HTTP_HTTPS(EInterestCategory.NETWORKING, "HTTP/HTTPS"), + RESTFUL_API(EInterestCategory.NETWORKING, "RESTful API"), + WEBSOCKET(EInterestCategory.NETWORKING, "WebSocket"), + + // Security + NETWORK_SECURITY(EInterestCategory.SECURITY, "네트워크 보안"), + WEB_SECURITY(EInterestCategory.SECURITY, "웹 보안"), + ENCRYPTION(EInterestCategory.SECURITY, "암호화"), + AUTHENTICATION(EInterestCategory.SECURITY, "인증"), + + // Game Dev + UNITY(EInterestCategory.GAME_DEV, "Unity"), + UNREAL_ENGINE(EInterestCategory.GAME_DEV, "Unreal Engine"), + GAME_CSHARP(EInterestCategory.GAME_DEV, "C#"), + GAME_CPP(EInterestCategory.GAME_DEV, "C++"), + + // AR/VR/XR + ARKIT(EInterestCategory.AR_VR_XR, "ARKit"), + REALITYKIT(EInterestCategory.AR_VR_XR, "RealityKit"), + UNITY_AR(EInterestCategory.AR_VR_XR, "Unity AR"), + VR_DEVELOPMENT(EInterestCategory.AR_VR_XR, "VR Development"), + + // Embedded/IoT + C(EInterestCategory.EMBEDDED_IOT, "C"), + CPP(EInterestCategory.EMBEDDED_IOT, "C++"), + ARDUINO(EInterestCategory.EMBEDDED_IOT, "Arduino"), + RASPBERRY_PI(EInterestCategory.EMBEDDED_IOT, "Raspberry Pi"), + RTOS(EInterestCategory.EMBEDDED_IOT, "RTOS"), + + // Blockchain/Web3 + ETHEREUM(EInterestCategory.BLOCKCHAIN_WEB3, "이더리움"), + SMART_CONTRACT(EInterestCategory.BLOCKCHAIN_WEB3, "스마트 컨트랙트"), + SOLIDITY(EInterestCategory.BLOCKCHAIN_WEB3, "Solidity"), + WEB3(EInterestCategory.BLOCKCHAIN_WEB3, "Web3"), + BLOCKCHAIN_BASICS(EInterestCategory.BLOCKCHAIN_WEB3, "블록체인 기초"), + DAPP(EInterestCategory.BLOCKCHAIN_WEB3, "DApp"), + NFT(EInterestCategory.BLOCKCHAIN_WEB3, "NFT"), + CRYPTOCURRENCY(EInterestCategory.BLOCKCHAIN_WEB3, "암호화폐"), + + // QA/Test + JUNIT(EInterestCategory.QA_TEST, "JUnit"), + SELENIUM(EInterestCategory.QA_TEST, "Selenium"), + TEST_AUTOMATION(EInterestCategory.QA_TEST, "Test Automation"), + TDD(EInterestCategory.QA_TEST, "TDD"), + + // Product/UX + FIGMA(EInterestCategory.PRODUCT_UX, "Figma"), + SKETCH(EInterestCategory.PRODUCT_UX, "Sketch"), + ADOBE_XD(EInterestCategory.PRODUCT_UX, "Adobe XD"), + PROTOTYPING(EInterestCategory.PRODUCT_UX, "프로토타이핑"), + + // Architecture + MICROSERVICES(EInterestCategory.ARCHITECTURE, "Microservices"), + DDD(EInterestCategory.ARCHITECTURE, "DDD"), + DESIGN_PATTERNS(EInterestCategory.ARCHITECTURE, "Design Patterns"), + CLEAN_ARCHITECTURE(EInterestCategory.ARCHITECTURE, "Clean Architecture"); + + private final EInterestCategory category; + private final String displayName; + + public static List getKeywordsByCategory(EInterestCategory category) { + return Arrays.stream(values()) + .filter(keyword -> keyword.category == category) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/techfork/domain/user/enums/Role.java b/src/main/java/com/techfork/domain/useraccount/enums/Role.java similarity index 80% rename from src/main/java/com/techfork/domain/user/enums/Role.java rename to src/main/java/com/techfork/domain/useraccount/enums/Role.java index d81f2ec5..c9cc787a 100644 --- a/src/main/java/com/techfork/domain/user/enums/Role.java +++ b/src/main/java/com/techfork/domain/useraccount/enums/Role.java @@ -1,4 +1,4 @@ -package com.techfork.domain.user.enums; +package com.techfork.domain.useraccount.enums; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/techfork/domain/user/enums/SocialType.java b/src/main/java/com/techfork/domain/useraccount/enums/SocialType.java similarity index 91% rename from src/main/java/com/techfork/domain/user/enums/SocialType.java rename to src/main/java/com/techfork/domain/useraccount/enums/SocialType.java index bb85309b..81e15d6e 100644 --- a/src/main/java/com/techfork/domain/user/enums/SocialType.java +++ b/src/main/java/com/techfork/domain/useraccount/enums/SocialType.java @@ -1,4 +1,4 @@ -package com.techfork.domain.user.enums; +package com.techfork.domain.useraccount.enums; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/techfork/domain/user/enums/UserStatus.java b/src/main/java/com/techfork/domain/useraccount/enums/UserStatus.java similarity index 87% rename from src/main/java/com/techfork/domain/user/enums/UserStatus.java rename to src/main/java/com/techfork/domain/useraccount/enums/UserStatus.java index 9e8fa3f5..7fd30396 100644 --- a/src/main/java/com/techfork/domain/user/enums/UserStatus.java +++ b/src/main/java/com/techfork/domain/useraccount/enums/UserStatus.java @@ -1,4 +1,4 @@ -package com.techfork.domain.user.enums; +package com.techfork.domain.useraccount.enums; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/techfork/domain/user/exception/UserErrorCode.java b/src/main/java/com/techfork/domain/useraccount/exception/UserErrorCode.java similarity index 92% rename from src/main/java/com/techfork/domain/user/exception/UserErrorCode.java rename to src/main/java/com/techfork/domain/useraccount/exception/UserErrorCode.java index 75c2e034..0f380d0f 100644 --- a/src/main/java/com/techfork/domain/user/exception/UserErrorCode.java +++ b/src/main/java/com/techfork/domain/useraccount/exception/UserErrorCode.java @@ -1,26 +1,26 @@ -package com.techfork.domain.user.exception; - -import com.techfork.global.common.code.BaseCode; -import com.techfork.global.response.ReasonDTO; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; - -@RequiredArgsConstructor -public enum UserErrorCode implements BaseCode { - USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404_1", "사용자를 찾을 수 없습니다."), - INVALID_INTEREST_KEYWORD(HttpStatus.BAD_REQUEST, "USER400_1", "유효하지 않은 관심사 키워드입니다."), - ALREADY_WITHDRAWN(HttpStatus.BAD_REQUEST, "USER400_2", "이미 탈퇴한 회원입니다."); - - private final HttpStatus httpStatus; - private final String code; - private final String message; - - @Override - public ReasonDTO getReason() { - return ReasonDTO.builder() - .httpStatus(httpStatus) - .code(code) - .message(message) - .build(); - } -} +package com.techfork.domain.useraccount.exception; + +import com.techfork.global.common.code.BaseCode; +import com.techfork.global.response.ReasonDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +public enum UserErrorCode implements BaseCode { + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404_1", "사용자를 찾을 수 없습니다."), + INVALID_INTEREST_KEYWORD(HttpStatus.BAD_REQUEST, "USER400_1", "유효하지 않은 관심사 키워드입니다."), + ALREADY_WITHDRAWN(HttpStatus.BAD_REQUEST, "USER400_2", "이미 탈퇴한 회원입니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ReasonDTO getReason() { + return ReasonDTO.builder() + .httpStatus(httpStatus) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/techfork/domain/user/repository/UserInterestCategoryRepository.java b/src/main/java/com/techfork/domain/useraccount/repository/UserInterestCategoryRepository.java similarity index 81% rename from src/main/java/com/techfork/domain/user/repository/UserInterestCategoryRepository.java rename to src/main/java/com/techfork/domain/useraccount/repository/UserInterestCategoryRepository.java index 68b1a3cf..8edc4df2 100644 --- a/src/main/java/com/techfork/domain/user/repository/UserInterestCategoryRepository.java +++ b/src/main/java/com/techfork/domain/useraccount/repository/UserInterestCategoryRepository.java @@ -1,18 +1,18 @@ -package com.techfork.domain.user.repository; - -import com.techfork.domain.user.entity.UserInterestCategory; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.List; - -public interface UserInterestCategoryRepository extends JpaRepository { - @Query(""" - SELECT DISTINCT uic FROM UserInterestCategory uic - LEFT JOIN FETCH uic.keywords - WHERE uic.user.id = :userId - """) - List findByUserIdWithKeywords(@Param("userId") Long userId); - -} +package com.techfork.domain.useraccount.repository; + +import com.techfork.domain.useraccount.entity.UserInterestCategory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface UserInterestCategoryRepository extends JpaRepository { + @Query(""" + SELECT DISTINCT uic FROM UserInterestCategory uic + LEFT JOIN FETCH uic.keywords + WHERE uic.user.id = :userId + """) + List findByUserIdWithKeywords(@Param("userId") Long userId); + +} diff --git a/src/main/java/com/techfork/domain/user/repository/UserRepository.java b/src/main/java/com/techfork/domain/useraccount/repository/UserRepository.java similarity index 90% rename from src/main/java/com/techfork/domain/user/repository/UserRepository.java rename to src/main/java/com/techfork/domain/useraccount/repository/UserRepository.java index 58c82b6f..781fb6db 100644 --- a/src/main/java/com/techfork/domain/user/repository/UserRepository.java +++ b/src/main/java/com/techfork/domain/useraccount/repository/UserRepository.java @@ -1,52 +1,52 @@ -package com.techfork.domain.user.repository; - -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.SocialType; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -public interface UserRepository extends JpaRepository { - - Optional findBySocialTypeAndSocialId(SocialType socialType, String socialId); - - @Query(""" - SELECT DISTINCT u FROM User u - LEFT JOIN FETCH u.interestCategories - WHERE u.id = :userId - """) - Optional findByIdWithInterestCategories(@Param("userId") Long userId); - - /** - * 최근 특정 시간 이후 활동한 사용자 조회 - * (읽은 포스트, 북마크, 검색 기록 중 하나라도 있으면 활성 사용자) - * 탈퇴한 사용자는 제외 - */ - @Query(""" - SELECT DISTINCT u FROM User u - WHERE u.status != 'WITHDRAWN' - AND (EXISTS ( - SELECT 1 FROM ReadPost rp WHERE rp.user = u AND rp.readAt >= :since - ) OR EXISTS ( - SELECT 1 FROM Bookmark b WHERE b.user = u AND b.bookmarkedAt >= :since - ) OR EXISTS ( - SELECT 1 FROM SearchHistory sh WHERE sh.user = u AND sh.searchedAt >= :since - )) - """) - List findActiveUsersSince(@Param("since") LocalDateTime since); - - /** - * 관심사 카테고리와 함께 사용자 조회 (Fetch Join) - * 주의: keywords는 Multiple Bag Fetch 문제로 제외 (필요시 별도 쿼리) - */ - @Query(""" - SELECT DISTINCT u FROM User u - LEFT JOIN FETCH u.interestCategories - WHERE u.id IN :userIds - """) - List findAllWithInterestCategoriesByIds(@Param("userIds") List userIds); -} +package com.techfork.domain.useraccount.repository; + +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.SocialType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + + Optional findBySocialTypeAndSocialId(SocialType socialType, String socialId); + + @Query(""" + SELECT DISTINCT u FROM User u + LEFT JOIN FETCH u.interestCategories + WHERE u.id = :userId + """) + Optional findByIdWithInterestCategories(@Param("userId") Long userId); + + /** + * 최근 특정 시간 이후 활동한 사용자 조회 + * (읽은 포스트, 북마크, 검색 기록 중 하나라도 있으면 활성 사용자) + * 탈퇴한 사용자는 제외 + */ + @Query(""" + SELECT DISTINCT u FROM User u + WHERE u.status != 'WITHDRAWN' + AND (EXISTS ( + SELECT 1 FROM ReadPost rp WHERE rp.user = u AND rp.readAt >= :since + ) OR EXISTS ( + SELECT 1 FROM Bookmark b WHERE b.user = u AND b.bookmarkedAt >= :since + ) OR EXISTS ( + SELECT 1 FROM SearchHistory sh WHERE sh.user = u AND sh.searchedAt >= :since + )) + """) + List findActiveUsersSince(@Param("since") LocalDateTime since); + + /** + * 관심사 카테고리와 함께 사용자 조회 (Fetch Join) + * 주의: keywords는 Multiple Bag Fetch 문제로 제외 (필요시 별도 쿼리) + */ + @Query(""" + SELECT DISTINCT u FROM User u + LEFT JOIN FETCH u.interestCategories + WHERE u.id IN :userIds + """) + List findAllWithInterestCategoriesByIds(@Param("userIds") List userIds); +} diff --git a/src/main/java/com/techfork/domain/user/service/InterestCommandService.java b/src/main/java/com/techfork/domain/useraccount/service/InterestCommandService.java similarity index 79% rename from src/main/java/com/techfork/domain/user/service/InterestCommandService.java rename to src/main/java/com/techfork/domain/useraccount/service/InterestCommandService.java index 1a15f57b..fb18071a 100644 --- a/src/main/java/com/techfork/domain/user/service/InterestCommandService.java +++ b/src/main/java/com/techfork/domain/useraccount/service/InterestCommandService.java @@ -1,14 +1,15 @@ -package com.techfork.domain.user.service; +package com.techfork.domain.useraccount.service; -import com.techfork.domain.user.dto.SaveInterestRequest; -import com.techfork.domain.user.dto.UserInterestDto; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.entity.UserInterestCategory; -import com.techfork.domain.user.entity.UserInterestKeyword; -import com.techfork.domain.user.enums.EInterestCategory; -import com.techfork.domain.user.enums.EInterestKeyword; -import com.techfork.domain.user.exception.UserErrorCode; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.dto.SaveInterestRequest; +import com.techfork.domain.useraccount.dto.UserInterestDto; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.entity.UserInterestCategory; +import com.techfork.domain.useraccount.entity.UserInterestKeyword; +import com.techfork.domain.useraccount.enums.EInterestCategory; +import com.techfork.domain.useraccount.enums.EInterestKeyword; +import com.techfork.domain.useraccount.exception.UserErrorCode; +import com.techfork.domain.useraccount.repository.UserRepository; +import com.techfork.domain.personalization.service.PersonalizationProfileService; import com.techfork.global.exception.GeneralException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/techfork/domain/user/service/InterestQueryService.java b/src/main/java/com/techfork/domain/useraccount/service/InterestQueryService.java similarity index 67% rename from src/main/java/com/techfork/domain/user/service/InterestQueryService.java rename to src/main/java/com/techfork/domain/useraccount/service/InterestQueryService.java index 84aa5404..8237df5f 100644 --- a/src/main/java/com/techfork/domain/user/service/InterestQueryService.java +++ b/src/main/java/com/techfork/domain/useraccount/service/InterestQueryService.java @@ -1,47 +1,47 @@ -package com.techfork.domain.user.service; - -import com.techfork.domain.user.converter.InterestConverter; -import com.techfork.domain.user.dto.InterestListResponse; -import com.techfork.domain.user.dto.UserInterestDto; -import com.techfork.domain.user.dto.UserInterestResponse; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.entity.UserInterestCategory; -import com.techfork.domain.user.exception.UserErrorCode; -import com.techfork.domain.user.repository.UserInterestCategoryRepository; -import com.techfork.domain.user.repository.UserRepository; -import com.techfork.global.exception.GeneralException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class InterestQueryService { - - private final UserRepository userRepository; - private final UserInterestCategoryRepository userInterestCategoryRepository; - private final InterestConverter interestConverter; - - public InterestListResponse getAllInterests() { - return InterestListResponse.builder() - .categories(interestConverter.toInterestCategoryDtoList()) - .build(); - } - - public UserInterestResponse getUserInterests(Long userId) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); - - List categories = userInterestCategoryRepository.findByUserIdWithKeywords(user.getId()); - List userInterestDtos = interestConverter.toUserInterestDtoList(categories); - - return UserInterestResponse.builder() - .interests(userInterestDtos) - .build(); - } -} +package com.techfork.domain.useraccount.service; + +import com.techfork.domain.useraccount.converter.InterestConverter; +import com.techfork.domain.useraccount.dto.InterestListResponse; +import com.techfork.domain.useraccount.dto.UserInterestDto; +import com.techfork.domain.useraccount.dto.UserInterestResponse; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.entity.UserInterestCategory; +import com.techfork.domain.useraccount.exception.UserErrorCode; +import com.techfork.domain.useraccount.repository.UserInterestCategoryRepository; +import com.techfork.domain.useraccount.repository.UserRepository; +import com.techfork.global.exception.GeneralException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class InterestQueryService { + + private final UserRepository userRepository; + private final UserInterestCategoryRepository userInterestCategoryRepository; + private final InterestConverter interestConverter; + + public InterestListResponse getAllInterests() { + return InterestListResponse.builder() + .categories(interestConverter.toInterestCategoryDtoList()) + .build(); + } + + public UserInterestResponse getUserInterests(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); + + List categories = userInterestCategoryRepository.findByUserIdWithKeywords(user.getId()); + List userInterestDtos = interestConverter.toUserInterestDtoList(categories); + + return UserInterestResponse.builder() + .interests(userInterestDtos) + .build(); + } +} diff --git a/src/main/java/com/techfork/domain/user/service/UserCommandService.java b/src/main/java/com/techfork/domain/useraccount/service/UserCommandService.java similarity index 83% rename from src/main/java/com/techfork/domain/user/service/UserCommandService.java rename to src/main/java/com/techfork/domain/useraccount/service/UserCommandService.java index 4fe3f9aa..598cf857 100644 --- a/src/main/java/com/techfork/domain/user/service/UserCommandService.java +++ b/src/main/java/com/techfork/domain/useraccount/service/UserCommandService.java @@ -1,11 +1,11 @@ -package com.techfork.domain.user.service; - -import com.techfork.domain.user.dto.OnboardingRequest; -import com.techfork.domain.user.dto.SaveInterestRequest; -import com.techfork.domain.user.dto.UpdateAccountProfileRequest; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.exception.UserErrorCode; -import com.techfork.domain.user.repository.UserRepository; +package com.techfork.domain.useraccount.service; + +import com.techfork.domain.useraccount.dto.OnboardingRequest; +import com.techfork.domain.useraccount.dto.SaveInterestRequest; +import com.techfork.domain.useraccount.dto.UpdateAccountProfileRequest; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.exception.UserErrorCode; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.exception.GeneralException; import com.techfork.global.security.auth.service.UserAuthCacheService; import jakarta.validation.Valid; diff --git a/src/main/java/com/techfork/domain/user/service/UserQueryService.java b/src/main/java/com/techfork/domain/useraccount/service/UserQueryService.java similarity index 68% rename from src/main/java/com/techfork/domain/user/service/UserQueryService.java rename to src/main/java/com/techfork/domain/useraccount/service/UserQueryService.java index 6a5458b1..5549a8a2 100644 --- a/src/main/java/com/techfork/domain/user/service/UserQueryService.java +++ b/src/main/java/com/techfork/domain/useraccount/service/UserQueryService.java @@ -1,10 +1,10 @@ -package com.techfork.domain.user.service; +package com.techfork.domain.useraccount.service; -import com.techfork.domain.user.converter.UserConverter; -import com.techfork.domain.user.dto.AccountProfileResponse; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.exception.UserErrorCode; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.converter.UserConverter; +import com.techfork.domain.useraccount.dto.AccountProfileResponse; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.exception.UserErrorCode; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.exception.GeneralException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/techfork/global/config/ElasticsearchCacheManager.java b/src/main/java/com/techfork/global/config/ElasticsearchCacheManager.java index 3b364f40..b76462ef 100644 --- a/src/main/java/com/techfork/global/config/ElasticsearchCacheManager.java +++ b/src/main/java/com/techfork/global/config/ElasticsearchCacheManager.java @@ -5,7 +5,7 @@ import co.elastic.clients.json.JsonData; import com.techfork.domain.post.document.PostDocument; import com.techfork.domain.recommendation.config.RecommendationProperties; -import com.techfork.domain.user.document.PersonalizationProfileDocument; +import com.techfork.domain.personalization.document.PersonalizationProfileDocument; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; diff --git a/src/main/java/com/techfork/global/security/auth/service/UserAuthCacheService.java b/src/main/java/com/techfork/global/security/auth/service/UserAuthCacheService.java index ae418785..3bb171fe 100644 --- a/src/main/java/com/techfork/global/security/auth/service/UserAuthCacheService.java +++ b/src/main/java/com/techfork/global/security/auth/service/UserAuthCacheService.java @@ -1,8 +1,8 @@ package com.techfork.global.security.auth.service; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.UserStatus; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.UserStatus; import com.techfork.global.constant.RedisKey; import com.techfork.global.security.oauth.UserPrincipal; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/techfork/global/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/techfork/global/security/filter/JwtAuthenticationFilter.java index bbf09ddd..a0e3be92 100644 --- a/src/main/java/com/techfork/global/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/techfork/global/security/filter/JwtAuthenticationFilter.java @@ -1,9 +1,9 @@ package com.techfork.global.security.filter; import com.techfork.domain.auth.exception.AuthErrorCode; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.UserStatus; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.UserStatus; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.constant.Constants; import com.techfork.global.constant.MdcKey; import com.techfork.global.exception.GeneralException; diff --git a/src/main/java/com/techfork/global/security/handler/login/OAuth2AuthenticationSuccessHandler.java b/src/main/java/com/techfork/global/security/handler/login/OAuth2AuthenticationSuccessHandler.java index 1a586f22..a78331ef 100644 --- a/src/main/java/com/techfork/global/security/handler/login/OAuth2AuthenticationSuccessHandler.java +++ b/src/main/java/com/techfork/global/security/handler/login/OAuth2AuthenticationSuccessHandler.java @@ -1,6 +1,6 @@ package com.techfork.global.security.handler.login; -import com.techfork.domain.user.enums.UserStatus; +import com.techfork.domain.useraccount.enums.UserStatus; import com.techfork.global.security.auth.service.RefreshTokenService; import com.techfork.global.security.jwt.JwtDTO; import com.techfork.global.security.jwt.JwtProperties; diff --git a/src/main/java/com/techfork/global/security/jwt/JwtUtil.java b/src/main/java/com/techfork/global/security/jwt/JwtUtil.java index bb72f8c0..b417a372 100644 --- a/src/main/java/com/techfork/global/security/jwt/JwtUtil.java +++ b/src/main/java/com/techfork/global/security/jwt/JwtUtil.java @@ -1,7 +1,7 @@ package com.techfork.global.security.jwt; import com.techfork.domain.auth.exception.AuthErrorCode; -import com.techfork.domain.user.enums.Role; +import com.techfork.domain.useraccount.enums.Role; import com.techfork.global.exception.GeneralException; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; diff --git a/src/main/java/com/techfork/global/security/oauth/CustomOidcUserService.java b/src/main/java/com/techfork/global/security/oauth/CustomOidcUserService.java index 88138d10..9a2a968e 100644 --- a/src/main/java/com/techfork/global/security/oauth/CustomOidcUserService.java +++ b/src/main/java/com/techfork/global/security/oauth/CustomOidcUserService.java @@ -1,8 +1,8 @@ package com.techfork.global.security.oauth; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; diff --git a/src/main/java/com/techfork/global/security/oauth/UserPrincipal.java b/src/main/java/com/techfork/global/security/oauth/UserPrincipal.java index f23dadf4..a9e2a284 100644 --- a/src/main/java/com/techfork/global/security/oauth/UserPrincipal.java +++ b/src/main/java/com/techfork/global/security/oauth/UserPrincipal.java @@ -1,8 +1,8 @@ package com.techfork.global.security.oauth; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.UserStatus; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.UserStatus; import lombok.Builder; import lombok.Getter; import org.springframework.security.core.GrantedAuthority; diff --git a/src/test/java/com/techfork/domain/activity/controller/ActivityControllerIntegrationTest.java b/src/test/java/com/techfork/domain/activity/controller/ActivityControllerIntegrationTest.java index aa17b427..a020166c 100644 --- a/src/test/java/com/techfork/domain/activity/controller/ActivityControllerIntegrationTest.java +++ b/src/test/java/com/techfork/domain/activity/controller/ActivityControllerIntegrationTest.java @@ -14,10 +14,10 @@ import com.techfork.domain.post.repository.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.common.IntegrationTestBase; import com.techfork.global.security.jwt.JwtDTO; import com.techfork.global.security.jwt.JwtUtil; diff --git a/src/test/java/com/techfork/domain/activity/repository/BookmarkRepositoryTest.java b/src/test/java/com/techfork/domain/activity/repository/BookmarkRepositoryTest.java index 0748c1a4..61262803 100644 --- a/src/test/java/com/techfork/domain/activity/repository/BookmarkRepositoryTest.java +++ b/src/test/java/com/techfork/domain/activity/repository/BookmarkRepositoryTest.java @@ -6,9 +6,9 @@ import com.techfork.domain.post.repository.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/com/techfork/domain/activity/repository/ReadPostRepositoryTest.java b/src/test/java/com/techfork/domain/activity/repository/ReadPostRepositoryTest.java index 0396de0b..ae73f4a5 100644 --- a/src/test/java/com/techfork/domain/activity/repository/ReadPostRepositoryTest.java +++ b/src/test/java/com/techfork/domain/activity/repository/ReadPostRepositoryTest.java @@ -5,9 +5,9 @@ import com.techfork.domain.post.repository.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/com/techfork/domain/activity/repository/SearchHistoryRepositoryTest.java b/src/test/java/com/techfork/domain/activity/repository/SearchHistoryRepositoryTest.java index a01dcb6e..83550457 100644 --- a/src/test/java/com/techfork/domain/activity/repository/SearchHistoryRepositoryTest.java +++ b/src/test/java/com/techfork/domain/activity/repository/SearchHistoryRepositoryTest.java @@ -1,9 +1,9 @@ package com.techfork.domain.activity.repository; import com.techfork.domain.activity.entity.SearchHistory; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/com/techfork/domain/activity/service/ActivityCommandServiceTest.java b/src/test/java/com/techfork/domain/activity/service/ActivityCommandServiceTest.java index 68211fd2..474a266d 100644 --- a/src/test/java/com/techfork/domain/activity/service/ActivityCommandServiceTest.java +++ b/src/test/java/com/techfork/domain/activity/service/ActivityCommandServiceTest.java @@ -14,9 +14,9 @@ import com.techfork.domain.post.exception.PostErrorCode; import com.techfork.domain.post.repository.PostRepository; import com.techfork.domain.source.entity.TechBlog; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.exception.UserErrorCode; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.exception.UserErrorCode; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.exception.GeneralException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/techfork/domain/activity/service/ActivityQueryServiceTest.java b/src/test/java/com/techfork/domain/activity/service/ActivityQueryServiceTest.java index 7e22b73b..026066d9 100644 --- a/src/test/java/com/techfork/domain/activity/service/ActivityQueryServiceTest.java +++ b/src/test/java/com/techfork/domain/activity/service/ActivityQueryServiceTest.java @@ -10,9 +10,9 @@ import com.techfork.domain.post.entity.Post; import com.techfork.domain.post.entity.PostKeyword; import com.techfork.domain.post.repository.PostKeywordRepository; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.exception.UserErrorCode; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.exception.UserErrorCode; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.exception.GeneralException; import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; import org.junit.jupiter.api.AfterAll; diff --git a/src/test/java/com/techfork/domain/admin/controller/AdminControllerIntegrationTest.java b/src/test/java/com/techfork/domain/admin/controller/AdminControllerIntegrationTest.java index fc625a4f..15f0e38a 100644 --- a/src/test/java/com/techfork/domain/admin/controller/AdminControllerIntegrationTest.java +++ b/src/test/java/com/techfork/domain/admin/controller/AdminControllerIntegrationTest.java @@ -2,10 +2,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.techfork.domain.auth.exception.AuthErrorCode; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.common.IntegrationTestBase; import com.techfork.global.security.jwt.JwtDTO; import com.techfork.global.security.jwt.JwtUtil; diff --git a/src/test/java/com/techfork/domain/auth/controller/AuthControllerIntegrationTest.java b/src/test/java/com/techfork/domain/auth/controller/AuthControllerIntegrationTest.java index 0ca67214..a9264f2f 100644 --- a/src/test/java/com/techfork/domain/auth/controller/AuthControllerIntegrationTest.java +++ b/src/test/java/com/techfork/domain/auth/controller/AuthControllerIntegrationTest.java @@ -5,11 +5,11 @@ import com.github.tomakehurst.wiremock.client.WireMock; import com.techfork.domain.auth.dto.KakaoLoginRequest; import com.techfork.domain.auth.exception.AuthErrorCode; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.enums.UserStatus; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.enums.UserStatus; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.common.IntegrationTestBase; import com.techfork.global.security.auth.service.RefreshTokenService; import com.techfork.global.security.jwt.JwtDTO; diff --git a/src/test/java/com/techfork/domain/auth/service/AuthServiceTest.java b/src/test/java/com/techfork/domain/auth/service/AuthServiceTest.java index 2d92cb67..5fdef67f 100644 --- a/src/test/java/com/techfork/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/com/techfork/domain/auth/service/AuthServiceTest.java @@ -6,11 +6,11 @@ import com.techfork.domain.auth.dto.TokenRefreshResponse; import com.techfork.domain.auth.dto.kakao.KakaoUserInfoResponse; import com.techfork.domain.auth.exception.AuthErrorCode; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.enums.UserStatus; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.enums.UserStatus; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.exception.GeneralException; import com.techfork.global.security.auth.service.RefreshTokenService; import com.techfork.global.security.auth.service.UserAuthCacheService; diff --git a/src/test/java/com/techfork/domain/user/service/PersonalizationProfileServiceTest.java b/src/test/java/com/techfork/domain/personalization/service/PersonalizationProfileServiceTest.java similarity index 94% rename from src/test/java/com/techfork/domain/user/service/PersonalizationProfileServiceTest.java rename to src/test/java/com/techfork/domain/personalization/service/PersonalizationProfileServiceTest.java index 75ab76bb..6086444d 100644 --- a/src/test/java/com/techfork/domain/user/service/PersonalizationProfileServiceTest.java +++ b/src/test/java/com/techfork/domain/personalization/service/PersonalizationProfileServiceTest.java @@ -1,4 +1,4 @@ -package com.techfork.domain.user.service; +package com.techfork.domain.personalization.service; import com.techfork.domain.activity.entity.Bookmark; import com.techfork.domain.activity.entity.ReadPost; @@ -9,16 +9,16 @@ import com.techfork.domain.post.entity.Post; import com.techfork.domain.post.entity.PostKeyword; import com.techfork.domain.recommendation.service.RecommendationService; -import com.techfork.domain.user.document.PersonalizationProfileDocument; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.entity.UserInterestCategory; -import com.techfork.domain.user.entity.UserInterestKeyword; -import com.techfork.domain.user.enums.EInterestCategory; -import com.techfork.domain.user.enums.EInterestKeyword; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserInterestCategoryRepository; -import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.personalization.document.PersonalizationProfileDocument; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.entity.UserInterestCategory; +import com.techfork.domain.useraccount.entity.UserInterestKeyword; +import com.techfork.domain.useraccount.enums.EInterestCategory; +import com.techfork.domain.useraccount.enums.EInterestKeyword; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserInterestCategoryRepository; +import com.techfork.domain.personalization.repository.PersonalizationProfileDocumentRepository; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.llm.EmbeddingClient; import com.techfork.global.llm.LlmClient; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/com/techfork/domain/post/controller/PostControllerIntegrationTest.java b/src/test/java/com/techfork/domain/post/controller/PostControllerIntegrationTest.java index cf82d295..ea9a8c28 100644 --- a/src/test/java/com/techfork/domain/post/controller/PostControllerIntegrationTest.java +++ b/src/test/java/com/techfork/domain/post/controller/PostControllerIntegrationTest.java @@ -8,10 +8,10 @@ import com.techfork.domain.post.repository.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.common.IntegrationTestBase; import com.techfork.global.security.jwt.JwtUtil; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/com/techfork/domain/post/controller/PostControllerV2IntegrationTest.java b/src/test/java/com/techfork/domain/post/controller/PostControllerV2IntegrationTest.java index be04f3ea..a9b760b0 100644 --- a/src/test/java/com/techfork/domain/post/controller/PostControllerV2IntegrationTest.java +++ b/src/test/java/com/techfork/domain/post/controller/PostControllerV2IntegrationTest.java @@ -6,10 +6,10 @@ import com.techfork.domain.post.repository.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.common.IntegrationTestBase; import com.techfork.global.security.jwt.JwtUtil; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/com/techfork/domain/recommendation/controller/RecommendationControllerIntegrationTest.java b/src/test/java/com/techfork/domain/recommendation/controller/RecommendationControllerIntegrationTest.java index dfa54750..981ebd55 100644 --- a/src/test/java/com/techfork/domain/recommendation/controller/RecommendationControllerIntegrationTest.java +++ b/src/test/java/com/techfork/domain/recommendation/controller/RecommendationControllerIntegrationTest.java @@ -9,10 +9,10 @@ import com.techfork.domain.recommendation.repository.RecommendedPostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.common.IntegrationTestBase; import com.techfork.global.security.jwt.JwtDTO; import com.techfork.global.security.jwt.JwtUtil; diff --git a/src/test/java/com/techfork/domain/recommendation/converter/RecommendationConverterTest.java b/src/test/java/com/techfork/domain/recommendation/converter/RecommendationConverterTest.java index 6208f93c..3231136c 100644 --- a/src/test/java/com/techfork/domain/recommendation/converter/RecommendationConverterTest.java +++ b/src/test/java/com/techfork/domain/recommendation/converter/RecommendationConverterTest.java @@ -4,8 +4,8 @@ import com.techfork.domain.recommendation.dto.RecommendedPostDto; import com.techfork.domain.recommendation.entity.RecommendedPost; import com.techfork.domain.source.entity.TechBlog; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.SocialType; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.SocialType; import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/techfork/domain/recommendation/service/RecommendationQueryServiceTest.java b/src/test/java/com/techfork/domain/recommendation/service/RecommendationQueryServiceTest.java index d837d483..a777a76e 100644 --- a/src/test/java/com/techfork/domain/recommendation/service/RecommendationQueryServiceTest.java +++ b/src/test/java/com/techfork/domain/recommendation/service/RecommendationQueryServiceTest.java @@ -8,9 +8,9 @@ import com.techfork.domain.recommendation.entity.RecommendedPost; import com.techfork.domain.recommendation.repository.RecommendedPostRepository; import com.techfork.domain.source.entity.TechBlog; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/techfork/domain/user/controller/OnboardingControllerIntegrationTest.java b/src/test/java/com/techfork/domain/useraccount/controller/OnboardingControllerIntegrationTest.java similarity index 96% rename from src/test/java/com/techfork/domain/user/controller/OnboardingControllerIntegrationTest.java rename to src/test/java/com/techfork/domain/useraccount/controller/OnboardingControllerIntegrationTest.java index 61ea6ec0..7f714d70 100644 --- a/src/test/java/com/techfork/domain/user/controller/OnboardingControllerIntegrationTest.java +++ b/src/test/java/com/techfork/domain/useraccount/controller/OnboardingControllerIntegrationTest.java @@ -1,12 +1,12 @@ -package com.techfork.domain.user.controller; +package com.techfork.domain.useraccount.controller; import com.fasterxml.jackson.databind.ObjectMapper; -import com.techfork.domain.user.dto.OnboardingRequest; -import com.techfork.domain.user.dto.UserInterestDto; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.dto.OnboardingRequest; +import com.techfork.domain.useraccount.dto.UserInterestDto; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.common.IntegrationTestBase; import com.techfork.global.llm.EmbeddingClient; import com.techfork.global.llm.LlmClient; diff --git a/src/test/java/com/techfork/domain/user/controller/UserControllerIntegrationTest.java b/src/test/java/com/techfork/domain/useraccount/controller/UserControllerIntegrationTest.java similarity index 95% rename from src/test/java/com/techfork/domain/user/controller/UserControllerIntegrationTest.java rename to src/test/java/com/techfork/domain/useraccount/controller/UserControllerIntegrationTest.java index f479b460..4fc268a9 100644 --- a/src/test/java/com/techfork/domain/user/controller/UserControllerIntegrationTest.java +++ b/src/test/java/com/techfork/domain/useraccount/controller/UserControllerIntegrationTest.java @@ -1,12 +1,12 @@ -package com.techfork.domain.user.controller; +package com.techfork.domain.useraccount.controller; import com.fasterxml.jackson.databind.ObjectMapper; -import com.techfork.domain.user.dto.UpdateAccountProfileRequest; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.enums.UserStatus; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.dto.UpdateAccountProfileRequest; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.enums.UserStatus; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.common.IntegrationTestBase; import com.techfork.global.security.jwt.JwtDTO; import com.techfork.global.security.jwt.JwtUtil; diff --git a/src/test/java/com/techfork/domain/user/repository/UserInterestCategoryRepositoryTest.java b/src/test/java/com/techfork/domain/useraccount/repository/UserInterestCategoryRepositoryTest.java similarity index 95% rename from src/test/java/com/techfork/domain/user/repository/UserInterestCategoryRepositoryTest.java rename to src/test/java/com/techfork/domain/useraccount/repository/UserInterestCategoryRepositoryTest.java index f0d36570..6e3a5b72 100644 --- a/src/test/java/com/techfork/domain/user/repository/UserInterestCategoryRepositoryTest.java +++ b/src/test/java/com/techfork/domain/useraccount/repository/UserInterestCategoryRepositoryTest.java @@ -1,11 +1,11 @@ -package com.techfork.domain.user.repository; - -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.entity.UserInterestCategory; -import com.techfork.domain.user.entity.UserInterestKeyword; -import com.techfork.domain.user.enums.EInterestCategory; -import com.techfork.domain.user.enums.EInterestKeyword; -import com.techfork.domain.user.enums.SocialType; +package com.techfork.domain.useraccount.repository; + +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.entity.UserInterestCategory; +import com.techfork.domain.useraccount.entity.UserInterestKeyword; +import com.techfork.domain.useraccount.enums.EInterestCategory; +import com.techfork.domain.useraccount.enums.EInterestKeyword; +import com.techfork.domain.useraccount.enums.SocialType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/techfork/domain/user/repository/UserRepositoryTest.java b/src/test/java/com/techfork/domain/useraccount/repository/UserRepositoryTest.java similarity index 97% rename from src/test/java/com/techfork/domain/user/repository/UserRepositoryTest.java rename to src/test/java/com/techfork/domain/useraccount/repository/UserRepositoryTest.java index dbc95543..211cd111 100644 --- a/src/test/java/com/techfork/domain/user/repository/UserRepositoryTest.java +++ b/src/test/java/com/techfork/domain/useraccount/repository/UserRepositoryTest.java @@ -1,4 +1,4 @@ -package com.techfork.domain.user.repository; +package com.techfork.domain.useraccount.repository; import com.techfork.domain.activity.entity.ReadPost; import com.techfork.domain.activity.entity.Bookmark; @@ -10,10 +10,10 @@ import com.techfork.domain.post.repository.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.entity.UserInterestCategory; -import com.techfork.domain.user.enums.EInterestCategory; -import com.techfork.domain.user.enums.SocialType; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.entity.UserInterestCategory; +import com.techfork.domain.useraccount.enums.EInterestCategory; +import com.techfork.domain.useraccount.enums.SocialType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/techfork/domain/user/service/InterestCommandServiceTest.java b/src/test/java/com/techfork/domain/useraccount/service/InterestCommandServiceTest.java similarity index 93% rename from src/test/java/com/techfork/domain/user/service/InterestCommandServiceTest.java rename to src/test/java/com/techfork/domain/useraccount/service/InterestCommandServiceTest.java index edca7343..9399c716 100644 --- a/src/test/java/com/techfork/domain/user/service/InterestCommandServiceTest.java +++ b/src/test/java/com/techfork/domain/useraccount/service/InterestCommandServiceTest.java @@ -1,12 +1,13 @@ -package com.techfork.domain.user.service; - -import com.techfork.domain.user.dto.SaveInterestRequest; -import com.techfork.domain.user.dto.UserInterestDto; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.entity.UserInterestCategory; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.exception.UserErrorCode; -import com.techfork.domain.user.repository.UserRepository; +package com.techfork.domain.useraccount.service; + +import com.techfork.domain.useraccount.dto.SaveInterestRequest; +import com.techfork.domain.useraccount.dto.UserInterestDto; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.entity.UserInterestCategory; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.exception.UserErrorCode; +import com.techfork.domain.useraccount.repository.UserRepository; +import com.techfork.domain.personalization.service.PersonalizationProfileService; import com.techfork.global.exception.GeneralException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/techfork/domain/user/service/UserCommandServiceTest.java b/src/test/java/com/techfork/domain/useraccount/service/UserCommandServiceTest.java similarity index 95% rename from src/test/java/com/techfork/domain/user/service/UserCommandServiceTest.java rename to src/test/java/com/techfork/domain/useraccount/service/UserCommandServiceTest.java index 5dde77cf..16240912 100644 --- a/src/test/java/com/techfork/domain/user/service/UserCommandServiceTest.java +++ b/src/test/java/com/techfork/domain/useraccount/service/UserCommandServiceTest.java @@ -1,13 +1,13 @@ -package com.techfork.domain.user.service; - -import com.techfork.domain.user.dto.OnboardingRequest; -import com.techfork.domain.user.dto.SaveInterestRequest; -import com.techfork.domain.user.dto.UpdateAccountProfileRequest; -import com.techfork.domain.user.dto.UserInterestDto; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.exception.UserErrorCode; -import com.techfork.domain.user.repository.UserRepository; +package com.techfork.domain.useraccount.service; + +import com.techfork.domain.useraccount.dto.OnboardingRequest; +import com.techfork.domain.useraccount.dto.SaveInterestRequest; +import com.techfork.domain.useraccount.dto.UpdateAccountProfileRequest; +import com.techfork.domain.useraccount.dto.UserInterestDto; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.exception.UserErrorCode; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.exception.GeneralException; import com.techfork.global.security.auth.service.UserAuthCacheService; import org.junit.jupiter.api.BeforeEach; @@ -302,7 +302,7 @@ void withdrawUser_Success() { userCommandService.withdrawUser(userId); // Then - assertThat(testUser.getStatus()).isEqualTo(com.techfork.domain.user.enums.UserStatus.WITHDRAWN); + assertThat(testUser.getStatus()).isEqualTo(com.techfork.domain.useraccount.enums.UserStatus.WITHDRAWN); assertThat(testUser.isWithdrawn()).isTrue(); // 개인정보 익명화 확인 diff --git a/src/test/java/com/techfork/domain/user/service/UserQueryServiceTest.java b/src/test/java/com/techfork/domain/useraccount/service/UserQueryServiceTest.java similarity index 87% rename from src/test/java/com/techfork/domain/user/service/UserQueryServiceTest.java rename to src/test/java/com/techfork/domain/useraccount/service/UserQueryServiceTest.java index 0c6f83dc..6ca35e73 100644 --- a/src/test/java/com/techfork/domain/user/service/UserQueryServiceTest.java +++ b/src/test/java/com/techfork/domain/useraccount/service/UserQueryServiceTest.java @@ -1,11 +1,11 @@ -package com.techfork.domain.user.service; - -import com.techfork.domain.user.converter.UserConverter; -import com.techfork.domain.user.dto.AccountProfileResponse; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.exception.UserErrorCode; -import com.techfork.domain.user.repository.UserRepository; +package com.techfork.domain.useraccount.service; + +import com.techfork.domain.useraccount.converter.UserConverter; +import com.techfork.domain.useraccount.dto.AccountProfileResponse; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.exception.UserErrorCode; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.exception.GeneralException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/com/techfork/evaluation/recommendation/KValueComparisonTest.java b/src/test/java/com/techfork/evaluation/recommendation/KValueComparisonTest.java index 6e504d31..2b047ee4 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/KValueComparisonTest.java +++ b/src/test/java/com/techfork/evaluation/recommendation/KValueComparisonTest.java @@ -1,7 +1,7 @@ package com.techfork.evaluation.recommendation; import com.techfork.domain.recommendation.config.RecommendationProperties; -import com.techfork.domain.user.entity.User; +import com.techfork.domain.useraccount.entity.User; import lombok.Builder; import lombok.Getter; import lombok.extern.slf4j.Slf4j; diff --git a/src/test/java/com/techfork/evaluation/recommendation/LambdaOptimizationTest.java b/src/test/java/com/techfork/evaluation/recommendation/LambdaOptimizationTest.java index c236ede7..1287ba1a 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/LambdaOptimizationTest.java +++ b/src/test/java/com/techfork/evaluation/recommendation/LambdaOptimizationTest.java @@ -1,7 +1,7 @@ package com.techfork.evaluation.recommendation; import com.techfork.domain.recommendation.config.RecommendationProperties; -import com.techfork.domain.user.entity.User; +import com.techfork.domain.useraccount.entity.User; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; diff --git a/src/test/java/com/techfork/evaluation/recommendation/MmrCandidateSizeComparisonTest.java b/src/test/java/com/techfork/evaluation/recommendation/MmrCandidateSizeComparisonTest.java index c1565a41..0022ecf8 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/MmrCandidateSizeComparisonTest.java +++ b/src/test/java/com/techfork/evaluation/recommendation/MmrCandidateSizeComparisonTest.java @@ -1,7 +1,7 @@ package com.techfork.evaluation.recommendation; import com.techfork.domain.recommendation.config.RecommendationProperties; -import com.techfork.domain.user.entity.User; +import com.techfork.domain.useraccount.entity.User; import lombok.Builder; import lombok.Getter; import lombok.extern.slf4j.Slf4j; diff --git a/src/test/java/com/techfork/evaluation/recommendation/RecommendationConfigComparisonTest.java b/src/test/java/com/techfork/evaluation/recommendation/RecommendationConfigComparisonTest.java index f6e3d38c..044313b0 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/RecommendationConfigComparisonTest.java +++ b/src/test/java/com/techfork/evaluation/recommendation/RecommendationConfigComparisonTest.java @@ -1,6 +1,6 @@ package com.techfork.evaluation.recommendation; -import com.techfork.domain.user.entity.User; +import com.techfork.domain.useraccount.entity.User; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; diff --git a/src/test/java/com/techfork/evaluation/recommendation/RecommendationEvaluationService.java b/src/test/java/com/techfork/evaluation/recommendation/RecommendationEvaluationService.java index 0c38d173..99fad501 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/RecommendationEvaluationService.java +++ b/src/test/java/com/techfork/evaluation/recommendation/RecommendationEvaluationService.java @@ -17,9 +17,9 @@ import com.techfork.domain.recommendation.service.MmrService.MmrCandidate; import com.techfork.domain.recommendation.service.MmrService.MmrResult; import com.techfork.global.util.RrfScorer; -import com.techfork.domain.user.document.PersonalizationProfileDocument; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; +import com.techfork.domain.personalization.document.PersonalizationProfileDocument; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.personalization.repository.PersonalizationProfileDocumentRepository; import com.techfork.global.elasticsearch.query.VectorQueryBuilder; import com.techfork.global.util.TimeDecayStrategy; import lombok.extern.slf4j.Slf4j; diff --git a/src/test/java/com/techfork/evaluation/recommendation/RecommendationTestBase.java b/src/test/java/com/techfork/evaluation/recommendation/RecommendationTestBase.java index af42acf4..9a852789 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/RecommendationTestBase.java +++ b/src/test/java/com/techfork/evaluation/recommendation/RecommendationTestBase.java @@ -5,7 +5,7 @@ import com.techfork.domain.post.repository.PostDocumentRepository; import com.techfork.domain.recommendation.config.RecommendationProperties; import com.techfork.evaluation.recommendation.util.EvaluationFixtureLoader; -import com.techfork.domain.user.entity.User; +import com.techfork.domain.useraccount.entity.User; import com.techfork.global.common.IntegrationTestBase; import com.techfork.global.config.ElasticsearchCacheManager; import com.techfork.global.util.VectorUtil; @@ -51,7 +51,7 @@ public abstract class RecommendationTestBase extends IntegrationTestBase { @Autowired protected RecommendationEvaluationService evaluationService; @Autowired protected PostDocumentRepository postDocumentRepository; @Autowired protected ReadPostRepository readPostRepository; - @Autowired protected com.techfork.domain.user.repository.UserRepository userRepository; + @Autowired protected com.techfork.domain.useraccount.repository.UserRepository userRepository; @Autowired protected ElasticsearchClient elasticsearchClient; @Autowired protected ElasticsearchCacheManager elasticsearchCacheManager; diff --git a/src/test/java/com/techfork/evaluation/recommendation/TitleSummaryRatioOptimizationTest.java b/src/test/java/com/techfork/evaluation/recommendation/TitleSummaryRatioOptimizationTest.java index a6470a82..22a03e5b 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/TitleSummaryRatioOptimizationTest.java +++ b/src/test/java/com/techfork/evaluation/recommendation/TitleSummaryRatioOptimizationTest.java @@ -1,6 +1,6 @@ package com.techfork.evaluation.recommendation; -import com.techfork.domain.user.entity.User; +import com.techfork.domain.useraccount.entity.User; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; diff --git a/src/test/java/com/techfork/evaluation/recommendation/setup/UserDataSetupAndExporter.java b/src/test/java/com/techfork/evaluation/recommendation/setup/UserDataSetupAndExporter.java index c18cb223..926d1363 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/setup/UserDataSetupAndExporter.java +++ b/src/test/java/com/techfork/evaluation/recommendation/setup/UserDataSetupAndExporter.java @@ -7,11 +7,11 @@ import com.techfork.evaluation.recommendation.setup.components.TestDataGenerator; import com.techfork.evaluation.recommendation.setup.components.TestDataGenerator.UserCreationResult; import com.techfork.evaluation.recommendation.util.EvaluationFixtureLoader; -import com.techfork.domain.user.document.PersonalizationProfileDocument; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.EInterestCategory; -import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.personalization.document.PersonalizationProfileDocument; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.EInterestCategory; +import com.techfork.domain.personalization.repository.PersonalizationProfileDocumentRepository; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.common.IntegrationTestBase; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.*; diff --git a/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthGenerator.java b/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthGenerator.java index 11b83428..d9ecbeb2 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthGenerator.java +++ b/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthGenerator.java @@ -3,7 +3,7 @@ import com.techfork.domain.post.document.PostDocument; import com.techfork.domain.post.entity.Post; import com.techfork.domain.post.repository.PostDocumentRepository; -import com.techfork.domain.user.document.PersonalizationProfileDocument; +import com.techfork.domain.personalization.document.PersonalizationProfileDocument; import com.techfork.global.llm.LlmClient; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthValidator.java b/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthValidator.java index e64ffba8..fa7682dc 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthValidator.java +++ b/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthValidator.java @@ -1,6 +1,6 @@ package com.techfork.evaluation.recommendation.setup.components; -import com.techfork.domain.user.enums.EInterestCategory; +import com.techfork.domain.useraccount.enums.EInterestCategory; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; diff --git a/src/test/java/com/techfork/evaluation/recommendation/setup/components/PostMatcher.java b/src/test/java/com/techfork/evaluation/recommendation/setup/components/PostMatcher.java index 0976a7fc..ec79ace5 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/setup/components/PostMatcher.java +++ b/src/test/java/com/techfork/evaluation/recommendation/setup/components/PostMatcher.java @@ -2,7 +2,7 @@ import com.techfork.domain.post.entity.Post; import com.techfork.domain.post.repository.PostRepository; -import com.techfork.domain.user.enums.EInterestCategory; +import com.techfork.domain.useraccount.enums.EInterestCategory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; diff --git a/src/test/java/com/techfork/evaluation/recommendation/setup/components/TestDataGenerator.java b/src/test/java/com/techfork/evaluation/recommendation/setup/components/TestDataGenerator.java index d546ea80..624b1a63 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/setup/components/TestDataGenerator.java +++ b/src/test/java/com/techfork/evaluation/recommendation/setup/components/TestDataGenerator.java @@ -2,11 +2,11 @@ import com.techfork.domain.post.entity.Post; import com.techfork.domain.post.repository.PostRepository; -import com.techfork.domain.user.document.PersonalizationProfileDocument; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.EInterestCategory; -import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; -import com.techfork.domain.user.service.PersonalizationProfileService; +import com.techfork.domain.personalization.document.PersonalizationProfileDocument; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.EInterestCategory; +import com.techfork.domain.personalization.repository.PersonalizationProfileDocumentRepository; +import com.techfork.domain.personalization.service.PersonalizationProfileService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; diff --git a/src/test/java/com/techfork/evaluation/recommendation/setup/components/UserTestDataBuilder.java b/src/test/java/com/techfork/evaluation/recommendation/setup/components/UserTestDataBuilder.java index b2444835..72214f86 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/setup/components/UserTestDataBuilder.java +++ b/src/test/java/com/techfork/evaluation/recommendation/setup/components/UserTestDataBuilder.java @@ -7,14 +7,14 @@ import com.techfork.domain.activity.repository.BookmarkRepository; import com.techfork.domain.activity.repository.SearchHistoryRepository; import com.techfork.domain.post.entity.Post; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.entity.UserInterestCategory; -import com.techfork.domain.user.entity.UserInterestKeyword; -import com.techfork.domain.user.enums.EInterestCategory; -import com.techfork.domain.user.enums.EInterestKeyword; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserInterestCategoryRepository; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.entity.UserInterestCategory; +import com.techfork.domain.useraccount.entity.UserInterestKeyword; +import com.techfork.domain.useraccount.enums.EInterestCategory; +import com.techfork.domain.useraccount.enums.EInterestKeyword; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserInterestCategoryRepository; +import com.techfork.domain.useraccount.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; diff --git a/src/test/java/com/techfork/evaluation/recommendation/util/EvaluationFixtureLoader.java b/src/test/java/com/techfork/evaluation/recommendation/util/EvaluationFixtureLoader.java index ad75315e..9d875848 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/util/EvaluationFixtureLoader.java +++ b/src/test/java/com/techfork/evaluation/recommendation/util/EvaluationFixtureLoader.java @@ -12,16 +12,16 @@ import com.techfork.domain.post.repository.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; -import com.techfork.domain.user.document.PersonalizationProfileDocument; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.entity.UserInterestCategory; -import com.techfork.domain.user.entity.UserInterestKeyword; -import com.techfork.domain.user.enums.EInterestCategory; -import com.techfork.domain.user.enums.EInterestKeyword; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.personalization.document.PersonalizationProfileDocument; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.entity.UserInterestCategory; +import com.techfork.domain.useraccount.entity.UserInterestKeyword; +import com.techfork.domain.useraccount.enums.EInterestCategory; +import com.techfork.domain.useraccount.enums.EInterestKeyword; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.personalization.repository.PersonalizationProfileDocumentRepository; +import com.techfork.domain.useraccount.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.ClassPathResource; diff --git a/src/test/java/com/techfork/evaluation/search/SearchEvaluationTestBase.java b/src/test/java/com/techfork/evaluation/search/SearchEvaluationTestBase.java index ea7971b1..a5223468 100644 --- a/src/test/java/com/techfork/evaluation/search/SearchEvaluationTestBase.java +++ b/src/test/java/com/techfork/evaluation/search/SearchEvaluationTestBase.java @@ -11,7 +11,7 @@ import com.techfork.domain.search.config.GeneralSearchProperties; import com.techfork.domain.search.service.SearchService; import com.techfork.domain.search.service.SearchServiceImpl; -import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; +import com.techfork.domain.personalization.repository.PersonalizationProfileDocumentRepository; import com.techfork.evaluation.search.util.GroundTruthItem; import com.techfork.evaluation.search.util.SearchQualityService; import com.techfork.global.config.CloudflareThirdPartyThumbnailOptimizationProperties; diff --git a/src/test/java/com/techfork/evaluation/search/setup/PersonalizationProfileServiceTest.java b/src/test/java/com/techfork/evaluation/search/setup/PersonalizationProfileServiceTest.java index 8afc5559..9ece5d1f 100644 --- a/src/test/java/com/techfork/evaluation/search/setup/PersonalizationProfileServiceTest.java +++ b/src/test/java/com/techfork/evaluation/search/setup/PersonalizationProfileServiceTest.java @@ -1,12 +1,12 @@ package com.techfork.evaluation.search.setup; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.entity.UserInterestCategory; -import com.techfork.domain.user.enums.EInterestCategory; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserInterestCategoryRepository; -import com.techfork.domain.user.repository.UserRepository; -import com.techfork.domain.user.service.PersonalizationProfileService; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.entity.UserInterestCategory; +import com.techfork.domain.useraccount.enums.EInterestCategory; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserInterestCategoryRepository; +import com.techfork.domain.useraccount.repository.UserRepository; +import com.techfork.domain.personalization.service.PersonalizationProfileService; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; @@ -20,7 +20,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -import static com.techfork.domain.user.enums.EInterestCategory.*; +import static com.techfork.domain.useraccount.enums.EInterestCategory.*; @Tag("evaluation-setup") @Disabled("데이터 셋업용 - CI 제외") diff --git a/src/test/java/com/techfork/evaluation/search/setup/SearchGroundTruthGenerator.java b/src/test/java/com/techfork/evaluation/search/setup/SearchGroundTruthGenerator.java index dffe6371..8a85db64 100644 --- a/src/test/java/com/techfork/evaluation/search/setup/SearchGroundTruthGenerator.java +++ b/src/test/java/com/techfork/evaluation/search/setup/SearchGroundTruthGenerator.java @@ -8,7 +8,7 @@ import com.techfork.domain.search.dto.SearchResult; import com.techfork.domain.search.config.GeneralSearchProperties; import com.techfork.domain.search.service.SearchServiceImpl; -import com.techfork.domain.user.repository.PersonalizationProfileDocumentRepository; +import com.techfork.domain.personalization.repository.PersonalizationProfileDocumentRepository; import com.techfork.evaluation.recommendation.setup.components.FileExporter; import com.techfork.evaluation.search.util.GroundTruthItem; import com.techfork.global.config.CloudflareThirdPartyThumbnailOptimizationProperties; diff --git a/src/test/java/com/techfork/global/security/SecurityIntegrationTest.java b/src/test/java/com/techfork/global/security/SecurityIntegrationTest.java index 4af1d876..4cd61cd9 100644 --- a/src/test/java/com/techfork/global/security/SecurityIntegrationTest.java +++ b/src/test/java/com/techfork/global/security/SecurityIntegrationTest.java @@ -1,9 +1,9 @@ package com.techfork.global.security; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.common.IntegrationTestBase; import com.techfork.global.llm.EmbeddingClient; import com.techfork.global.llm.LlmClient; diff --git a/src/test/java/com/techfork/global/security/auth/service/UserAuthCacheServiceTest.java b/src/test/java/com/techfork/global/security/auth/service/UserAuthCacheServiceTest.java index 391b56b4..566366b1 100644 --- a/src/test/java/com/techfork/global/security/auth/service/UserAuthCacheServiceTest.java +++ b/src/test/java/com/techfork/global/security/auth/service/UserAuthCacheServiceTest.java @@ -1,9 +1,9 @@ package com.techfork.global.security.auth.service; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.enums.UserStatus; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.enums.UserStatus; import com.techfork.global.security.oauth.UserPrincipal; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/techfork/global/security/filter/JwtAuthenticationFilterTest.java b/src/test/java/com/techfork/global/security/filter/JwtAuthenticationFilterTest.java index f246d4fd..375148d2 100644 --- a/src/test/java/com/techfork/global/security/filter/JwtAuthenticationFilterTest.java +++ b/src/test/java/com/techfork/global/security/filter/JwtAuthenticationFilterTest.java @@ -1,10 +1,10 @@ package com.techfork.global.security.filter; -import com.techfork.domain.user.entity.User; -import com.techfork.domain.user.enums.Role; -import com.techfork.domain.user.enums.SocialType; -import com.techfork.domain.user.enums.UserStatus; -import com.techfork.domain.user.repository.UserRepository; +import com.techfork.domain.useraccount.entity.User; +import com.techfork.domain.useraccount.enums.Role; +import com.techfork.domain.useraccount.enums.SocialType; +import com.techfork.domain.useraccount.enums.UserStatus; +import com.techfork.domain.useraccount.repository.UserRepository; import com.techfork.global.security.auth.service.UserAuthCacheService; import com.techfork.global.security.jwt.JwtProperties; import com.techfork.global.security.jwt.JwtUtil; From 2e95b6093517e7380415c1e899492eae640a2f09 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Tue, 28 Apr 2026 21:51:37 +0900 Subject: [PATCH 5/6] =?UTF-8?q?docs:=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=B5=9C=EC=8B=A0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/domain-strategy.md | 14 ++++++------ docs/test-gap-analysis.md | 10 ++++----- docs/ubiquitous-language/README.md | 10 ++++----- .../personalization-profile.md | 14 ++++++------ docs/ubiquitous-language/search.md | 2 +- docs/ubiquitous-language/user-account.md | 22 +++++++++---------- docs/ubiquitous-language/user-profile.md | 3 ++- 7 files changed, 38 insertions(+), 37 deletions(-) diff --git a/docs/domain-strategy.md b/docs/domain-strategy.md index 5acee8cf..6d41d7bd 100644 --- a/docs/domain-strategy.md +++ b/docs/domain-strategy.md @@ -87,7 +87,7 @@ TechFork의 비즈니스 도메인은 다음으로 정의할 수 있다. - 다만 Auth / Security, Activity, Notification이 기대는 사용자 정체성 경계를 제공한다. - `Personalization Profile` 컨텍스트는 핵심 하위 도메인에 가깝다. - 개인화 프로필 생성, 프로필 벡터, 핵심 키워드, 재생성 정책은 검색/추천 품질의 중심이다. - - 현재 구현에서는 `PersonalizationProfileDocument`가 독립 aggregate보다 read model/projection 성격이 강하고, 생성 책임도 `domain/user` 아래 서비스에 함께 묶여 있다. + - 현재 구현에서는 `PersonalizationProfileDocument`가 독립 aggregate보다 read model/projection 성격이 강하고, 생성 책임은 `domain/personalization` 서비스가 담당한다. - `Post / Content` 컨텍스트 전체가 핵심은 아니다. - 단순 목록/상세 조회는 지원 하위 도메인이다. - 요약, 키워드 추출, 청크, 임베딩, 검색 문서화는 핵심 하위 도메인에 가깝다. @@ -119,26 +119,26 @@ TechFork의 비즈니스 도메인은 다음으로 정의할 수 있다. 현재 문서 기준 결론은 다음과 같다. - **전략 문서와 glossary에서는 `User Account`와 `Personalization Profile`을 별도 컨텍스트로 본다.** -- 다만 **현재 구현 패키지는 아직 `domain/user` 아래에 함께 존재한다.** +- 현재 구현은 `domain/useraccount`와 `domain/personalization`으로 물리 분리되어 있다. 의미: 1. `User` aggregate는 당분간 `User Account` 컨텍스트의 핵심 루트로 본다. 2. `PersonalizationProfileDocument`는 `Personalization Profile` 컨텍스트의 핵심 projection/read model로 본다. 3. Search/Recommendation과의 관계 해석은 `User Account`와 `Personalization Profile`을 분리해서 본다. -4. 패키지 분리, 포트 분리, 이벤트 분리는 후속 리팩터링 과제로 남긴다. +4. 패키지 분리는 완료되었고, 포트 분리와 이벤트 분리는 후속 리팩터링 과제로 남긴다. -현재 패키지를 유지하는 이유: +현재 상태 메모: -1. `domain/user` 내부에서 계정/온보딩/관심사/개인화 프로필 생성이 아직 함께 구현되어 있다. -2. `PersonalizationProfileService`는 Activity/Post/Recommendation을 조합하는 애플리케이션 서비스 성격이 강하지만, 이를 뒷받침하는 독립 패키지/포트/이벤트 경계는 아직 없다. +1. 패키지는 분리되었지만, `InterestCommandService` → `PersonalizationProfileService` 직접 호출은 아직 유지된다. +2. `PersonalizationProfileService`는 Activity/Post/Recommendation을 조합하는 애플리케이션 서비스 성격이 강하지만, 이를 뒷받침하는 독립 포트/이벤트 경계는 아직 없다. 3. `PersonalizationProfileDocument`는 독립 write aggregate보다 검색·추천용 read model에 가깝다. 향후 아래 조건이 충족되면 실제 패키지/구현도 둘로 나누는 것을 다시 검토한다. 1. `OnboardingCompleted`, `UserInterestsChanged`, `PersonalizedProfileGenerated` 같은 이벤트 흐름이 정착될 때 2. Search/Recommendation이 개인화 프로필을 전용 포트/Published Language로 소비할 때 -3. `domain/user` 내부가 계정/온보딩과 개인화 프로필 생성으로 패키지 수준에서 분리될 때 +3. 패키지 분리 이후 `useraccount` ↔ `personalization` direct dependency를 포트/이벤트로 치환할 때 4. 개인화 프로필이 독립 수명주기, 재생성 정책, 실패 복구 정책을 가진 모델로 커질 때 --- diff --git a/docs/test-gap-analysis.md b/docs/test-gap-analysis.md index 27aed7dd..d422b0dc 100644 --- a/docs/test-gap-analysis.md +++ b/docs/test-gap-analysis.md @@ -76,7 +76,7 @@ | domain/post | 4 | 72 | Controller/repository/query service 중심 | | domain/recommendation | 3 | 8 | 조회/컨버터 중심, 생성 로직 부족 | | domain/source | 10 | 38 | RSS/배치/스케줄러/락/웹훅 커버 좋음 | -| domain/user | 7 | 58 | User Account 중심. Personalization Profile 일반 테스트는 별도 안전망 부족 | +| domain/useraccount + domain/personalization | 8 | 61 | User Account service/controller/repository 커버 + Personalization Profile 기본 unit 안전망 확보 | | global | 6 | 33 | Security, cache, util, integration base | | evaluation | 27 | 18 | 검색/추천 품질 평가 및 fixture setup | @@ -258,19 +258,19 @@ ContentChunkerServiceTest | `InterestCommandServiceTest` | unit/mock | 관심사 저장, 기존 관심사 clear, invalid keyword category | | `UserCommandServiceTest` | unit/mock | 온보딩, 계정 프로필 수정, 탈퇴 | | `UserQueryServiceTest` | unit/mock | 계정 프로필 조회 | -| `evaluation/search/setup/PersonalizationProfileServiceTest` | evaluation-setup | 테스트 사용자 프로필 생성용 setup | +| `evaluation/search/setup/PersonalizationProfileServiceTest` | evaluation-setup | 테스트 사용자 개인화 프로필 생성용 setup | #### 평가 - **User Account 쪽**은 온보딩, 관심사, 계정 프로필, 탈퇴 흐름이 비교적 잘 보호되어 있다. -- 반면 **Personalization Profile 쪽**은 일반 테스트 lane에 `PersonalizationProfileService` 안전망이 거의 없다. -- 현재 있는 `PersonalizationProfileServiceTest`는 evaluation setup 용도라서, 개인화 프로필 리팩터링 안전망으로 보기 어렵다. +- 기존에는 **Personalization Profile 쪽** 일반 테스트 lane이 비어 있었지만, 이제 `PersonalizationProfileServiceTest` 기본 안전망이 추가되었다. +- evaluation setup용 `PersonalizationProfileServiceTest`는 여전히 별도 목적이므로, 일반 unit test lane과 구분해서 본다. #### 남은 갭 | 우선순위 | 갭 | 이유 | |---|---|---| -| P0 | `PersonalizationProfileServiceTest` 일반 단위 테스트 | Personalization Profile 생성은 추천/검색 개인화의 핵심이며 결합도가 높음 | +| 완료 | `PersonalizationProfileServiceTest` 일반 단위 테스트 | 활동 데이터 수집, 파싱, fallback, 저장, 추천 실패 격리 기본 흐름 보호 완료 | | P0 | LLM 응답 parsing 테스트 | `### PROFILE`, `### KEYWORDS` parsing 실패 시 품질/장애 영향 | | P0 | 관심사 변경 후 개인화 프로필 생성 트리거 검증 | `UserInterestsChanged` 이벤트 도입 전 현재 동작 보호 | | P1 | `UserTest` | User Account aggregate의 소셜 사용자 생성, 온보딩 ACTIVE, 탈퇴 anonymization, reactivate 규칙 보호 | diff --git a/docs/ubiquitous-language/README.md b/docs/ubiquitous-language/README.md index 83dd535f..944768e6 100644 --- a/docs/ubiquitous-language/README.md +++ b/docs/ubiquitous-language/README.md @@ -9,7 +9,7 @@ - **기준 단위는 패키지가 아니라 바운디드 컨텍스트다.** 다만 각 문서에 현재 owning package를 함께 적어 코드 탐색 경로를 명확히 한다. - **유비쿼터스 언어 문서는 도메인 전략과 왕복한다.** 새 용어가 나오면 먼저 이 문서를 고치고, 경계가 바뀌면 `domain-strategy.md`를 함께 조정한다. -- **전략 문서에서는 `User Account`와 `Personalization Profile`을 별도 컨텍스트로 본다.** 다만 현재 구현 패키지는 `domain/user` 아래에 함께 존재한다. +- **전략 문서에서는 `User Account`와 `Personalization Profile`을 별도 컨텍스트로 본다.** 현재 구현도 `domain/useraccount`와 `domain/personalization`으로 물리 분리되어 있다. - 레거시 코드명(`ScrabPost`, `searchWord`, `markAsisClicked`)은 허용하되, 문서/PR/API에서는 표준 용어를 우선 사용한다. - 각 컨텍스트 문서는 가능하면 아래 다섯 블록을 유지한다. 1. 표준 용어 @@ -26,8 +26,8 @@ |---|---|---|---| | Source / Ingestion | [`source-ingestion.md`](./source-ingestion.md) | `src/main/java/com/techfork/domain/source` | RSS 수집, 소스 블로그, 파이프라인 시작점 | | Post / Content | [`post-content.md`](./post-content.md) | `src/main/java/com/techfork/domain/post` | 기술 게시글 본문, 요약, 키워드, 검색 projection | -| User Account | [`user-account.md`](./user-account.md) | `src/main/java/com/techfork/domain/user` | 계정, 온보딩, 관심사, 계정 프로필 | -| Personalization Profile | [`personalization-profile.md`](./personalization-profile.md) | `src/main/java/com/techfork/domain/user` | 개인화 프로필 생성, 벡터, 핵심 키워드, 재생성 | +| User Account | [`user-account.md`](./user-account.md) | `src/main/java/com/techfork/domain/useraccount` | 계정, 온보딩, 관심사, 계정 프로필 | +| Personalization Profile | [`personalization-profile.md`](./personalization-profile.md) | `src/main/java/com/techfork/domain/personalization` | 개인화 프로필 생성, 벡터, 핵심 키워드, 재생성 | | Activity | [`activity.md`](./activity.md) | `src/main/java/com/techfork/domain/activity` | 읽기/검색/북마크 행동 기록 | | Search | [`search.md`](./search.md) | `src/main/java/com/techfork/domain/search` | query service / read model 중심 컨텍스트 | | Recommendation | [`recommendation.md`](./recommendation.md) | `src/main/java/com/techfork/domain/recommendation` | 추천 후보 탐색, 랭킹, 현재 추천 목록 | @@ -58,7 +58,7 @@ - 문서/PR/API에서 **“프로필” 단독 표현은 지양**한다. - UI/설정 화면은 `계정 프로필 수정`, 추천/검색 준비 상태는 `개인화 프로필 생성/재생성`으로 쓴다. -- `PersonalizationProfileDocument`는 현재 `domain/user` 패키지에 있으나, 개념상으로는 `Personalization Profile` language zone의 read model이다. +- `PersonalizationProfileDocument`는 `domain/personalization` 패키지의 read model/projection이다. --- @@ -82,7 +82,7 @@ 1. `domain-strategy.md`의 명칭과 glossary 문서명을 맞춘다. (`Auth` → `Auth / Security`) 2. `docs/ubiquitous-language.md`는 호환용 인덱스로 축소하고, 상세 용어는 이 디렉터리 문서에 모은다. 3. 전략 문서와 glossary는 `User Account` / `Personalization Profile`로 분리 유지한다. -4. 추후 실제 패키지 분리가 필요해지면 그 시점에 `domain/user` 하위 구조와 문서 구조를 다시 정렬한다. +4. 패키지 분리 이후에도 glossary와 전략 문서의 현재 상태 설명이 stale하지 않도록 함께 유지한다. ## 6. 내부 glossary를 채우는 기준 diff --git a/docs/ubiquitous-language/personalization-profile.md b/docs/ubiquitous-language/personalization-profile.md index b6e1c1ce..f4b0c479 100644 --- a/docs/ubiquitous-language/personalization-profile.md +++ b/docs/ubiquitous-language/personalization-profile.md @@ -1,12 +1,12 @@ # Personalization Profile > 활동 데이터와 관심사를 바탕으로 개인화 프로필을 생성하고, 검색/추천 입력 모델을 제공하는 개념적 바운디드 컨텍스트입니다. -> 현재 구현 패키지는 `domain/user`에 함께 존재하지만, 전략 문서에서는 `User Account`와 분리해서 봅니다. +> 현재 구현은 `domain/personalization`으로 분리되어 있으며, `User Account`와 물리적으로도 구분됩니다. ## Owning packages -- `src/main/java/com/techfork/domain/user` -- 관련 read model: `src/main/java/com/techfork/domain/user/document` +- `src/main/java/com/techfork/domain/personalization` +- 관련 read model: `src/main/java/com/techfork/domain/personalization/document` ## 표준 용어 @@ -17,7 +17,7 @@ | 프로필 벡터 | `profileVector` | `profileText`를 임베딩한 벡터 | | 핵심 키워드 | `keyKeywords` | LLM이 사용자 활동에서 추출한 3~5개 대표 관심 키워드 | | 활동 데이터 | `UserActivityData` | 관심사, 최근 읽은 기술 게시글, 북마크한 기술 게시글, 검색 기록을 합친 사용자 분석 입력 | -| 프로필 재생성 | `generateUserProfile`, `generateUserProfileSync` | 활동 변화 후 개인화 프로필을 다시 만드는 행위 | +| 프로필 재생성 | `generatePersonalizationProfile`, `generatePersonalizationProfileSync` | 활동 변화 후 개인화 프로필을 다시 만드는 행위 | ## 내부 glossary @@ -46,6 +46,6 @@ ## 주요 근거 파일 -- `src/main/java/com/techfork/domain/user/service/PersonalizationProfileService.java` -- `src/main/java/com/techfork/domain/user/document/PersonalizationProfileDocument.java` -- `src/main/java/com/techfork/domain/user/scheduler/PersonalizationProfileScheduler.java` +- `src/main/java/com/techfork/domain/personalization/service/PersonalizationProfileService.java` +- `src/main/java/com/techfork/domain/personalization/document/PersonalizationProfileDocument.java` +- `src/main/java/com/techfork/domain/personalization/scheduler/PersonalizationProfileScheduler.java` diff --git a/docs/ubiquitous-language/search.md b/docs/ubiquitous-language/search.md index b6dfea3e..b6b77d4d 100644 --- a/docs/ubiquitous-language/search.md +++ b/docs/ubiquitous-language/search.md @@ -5,7 +5,7 @@ ## Owning packages - `src/main/java/com/techfork/domain/search` -- 관련 read model: `src/main/java/com/techfork/domain/post/document`, `src/main/java/com/techfork/domain/user/document` +- 관련 read model: `src/main/java/com/techfork/domain/post/document`, `src/main/java/com/techfork/domain/personalization/document` ## 표준 용어 diff --git a/docs/ubiquitous-language/user-account.md b/docs/ubiquitous-language/user-account.md index f160b0dd..b318d40b 100644 --- a/docs/ubiquitous-language/user-account.md +++ b/docs/ubiquitous-language/user-account.md @@ -1,11 +1,11 @@ # User Account > 사용자 계정, 온보딩, 관심사, 계정 프로필을 다루는 개념적 바운디드 컨텍스트입니다. -> 현재 구현 패키지는 `domain/user`에 함께 존재하지만, 전략 문서에서는 `Personalization Profile`과 분리해서 봅니다. +> 현재 구현은 `domain/useraccount`로 분리되어 있으며, `Personalization Profile`과 물리적으로도 구분됩니다. ## Owning packages -- `src/main/java/com/techfork/domain/user` +- `src/main/java/com/techfork/domain/useraccount` ## 표준 용어 @@ -30,7 +30,7 @@ | 사용자 루트 | `User` | 계정/상태/기본 프로필/관심사를 소유하는 aggregate root | | 온보딩 완료 커맨드 | `UserCommandService.completeOnboarding` | 계정 정보 저장 + 관심사 저장 + 활성화 흐름 | | 관심사 교체 커맨드 | `InterestCommandService.updateUserInterests` | 관심사 전체를 갈아끼우는 흐름 | -| 계정 프로필 수정 | `UserCommandService.updateUserProfile` | 닉네임/자기소개 수정 흐름 | +| 계정 프로필 수정 | `UserCommandService.updateAccountProfile` | 닉네임/자기소개 수정 흐름 | | 탈퇴 처리 | `User.withdraw()` | 개인정보 익명화와 상태 변경을 수행하는 도메인 동작 | ## 혼동 금지 @@ -50,11 +50,11 @@ ## 주요 근거 파일 -- `src/main/java/com/techfork/domain/user/entity/User.java` -- `src/main/java/com/techfork/domain/user/enums/UserStatus.java` -- `src/main/java/com/techfork/domain/user/enums/EInterestCategory.java` -- `src/main/java/com/techfork/domain/user/enums/EInterestKeyword.java` -- `src/main/java/com/techfork/domain/user/service/UserCommandService.java` -- `src/main/java/com/techfork/domain/user/service/InterestCommandService.java` -- `src/main/java/com/techfork/domain/user/controller/OnboardingController.java` -- `src/main/java/com/techfork/domain/user/controller/UserController.java` +- `src/main/java/com/techfork/domain/useraccount/entity/User.java` +- `src/main/java/com/techfork/domain/useraccount/enums/UserStatus.java` +- `src/main/java/com/techfork/domain/useraccount/enums/EInterestCategory.java` +- `src/main/java/com/techfork/domain/useraccount/enums/EInterestKeyword.java` +- `src/main/java/com/techfork/domain/useraccount/service/UserCommandService.java` +- `src/main/java/com/techfork/domain/useraccount/service/InterestCommandService.java` +- `src/main/java/com/techfork/domain/useraccount/controller/OnboardingController.java` +- `src/main/java/com/techfork/domain/useraccount/controller/UserController.java` diff --git a/docs/ubiquitous-language/user-profile.md b/docs/ubiquitous-language/user-profile.md index d7c877ee..dcc65f24 100644 --- a/docs/ubiquitous-language/user-profile.md +++ b/docs/ubiquitous-language/user-profile.md @@ -6,4 +6,5 @@ - [User Account](./user-account.md) - [Personalization Profile](./personalization-profile.md) -현재 구현 패키지는 여전히 `src/main/java/com/techfork/domain/user`로 함께 묶여 있습니다. +현재 구현은 `src/main/java/com/techfork/domain/useraccount`와 +`src/main/java/com/techfork/domain/personalization`로 분리되어 있습니다. From f1c484bdeb97167f912914643532720f3b915053 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Tue, 28 Apr 2026 21:58:31 +0900 Subject: [PATCH 6/6] =?UTF-8?q?docs:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88=20=ED=85=9C=ED=94=8C=EB=A6=BF=20=EC=B5=9C=EC=8B=A0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...0\353\212\245-\352\260\234\354\204\240.md" | 17 ++++++++------- ...0\353\212\245-\352\265\254\355\230\204.md" | 17 ++++++++------- ...54\355\214\251\355\206\240\353\247\201.md" | 21 +++++++++++-------- ...4\352\267\270-\354\210\230\354\240\225.md" | 21 +++++++++++-------- ...4\355\212\270-\354\236\221\354\204\261.md" | 3 ++- 5 files changed, 44 insertions(+), 35 deletions(-) diff --git "a/.github/ISSUE_TEMPLATE/\352\270\260\353\212\245-\352\260\234\354\204\240.md" "b/.github/ISSUE_TEMPLATE/\352\270\260\353\212\245-\352\260\234\354\204\240.md" index 4b256504..17616d92 100644 --- "a/.github/ISSUE_TEMPLATE/\352\270\260\353\212\245-\352\260\234\354\204\240.md" +++ "b/.github/ISSUE_TEMPLATE/\352\270\260\353\212\245-\352\260\234\354\204\240.md" @@ -11,13 +11,14 @@ assignees: ''
-## 🏷️ 도메인 (해당하는 것에 체크) -- [ ] 📝 domain:post (게시글) -- [ ] 👤 domain:user (사용자) -- [ ] 🏢 domain:source (테크블로그 출처) -- [ ] 🔍 domain:search (검색) -- [ ] 🔔 domain:notification (알림) -- [ ] 📊 domain:recommendation (추천) +## 🏷️ 도메인 (해당하는 것에 체크) +- [ ] 📝 domain:post (게시글) +- [ ] 👤 domain:useraccount (사용자 계정) +- [ ] 🧠 domain:personalization (개인화 프로필) +- [ ] 🏢 domain:source (테크블로그 출처) +- [ ] 🔍 domain:search (검색) +- [ ] 🔔 domain:notification (알림) +- [ ] 📊 domain:recommendation (추천) - [ ] 🎯 domain:activity (사용자 활동) - [ ] 🔐 domain:auth (인증/보안) - [ ] 🌐 infra (인프라/배포) @@ -37,4 +38,4 @@ assignees: '' ## 💡 개선 이유 -
\ No newline at end of file +
diff --git "a/.github/ISSUE_TEMPLATE/\352\270\260\353\212\245-\352\265\254\355\230\204.md" "b/.github/ISSUE_TEMPLATE/\352\270\260\353\212\245-\352\265\254\355\230\204.md" index 86866d46..e38e1075 100644 --- "a/.github/ISSUE_TEMPLATE/\352\270\260\353\212\245-\352\265\254\355\230\204.md" +++ "b/.github/ISSUE_TEMPLATE/\352\270\260\353\212\245-\352\265\254\355\230\204.md" @@ -14,13 +14,14 @@ assignees: ''
-## 🏷️ 도메인 (해당하는 것에 체크) -- [ ] 📝 domain:post (게시글) -- [ ] 👤 domain:user (사용자) -- [ ] 🏢 domain:source (테크블로그 출처) -- [ ] 🔍 domain:search (검색) -- [ ] 🔔 domain:notification (알림) -- [ ] 📊 domain:recommendation (추천) +## 🏷️ 도메인 (해당하는 것에 체크) +- [ ] 📝 domain:post (게시글) +- [ ] 👤 domain:useraccount (사용자 계정) +- [ ] 🧠 domain:personalization (개인화 프로필) +- [ ] 🏢 domain:source (테크블로그 출처) +- [ ] 🔍 domain:search (검색) +- [ ] 🔔 domain:notification (알림) +- [ ] 📊 domain:recommendation (추천) - [ ] 🎯 domain:activity (사용자 활동) - [ ] 🔐 domain:auth (인증/보안) - [ ] 🌐 infra (인프라/배포) @@ -35,4 +36,4 @@ assignees: '' ## 💡 참고 사항 -
\ No newline at end of file +
diff --git "a/.github/ISSUE_TEMPLATE/\353\246\254\355\214\251\355\206\240\353\247\201.md" "b/.github/ISSUE_TEMPLATE/\353\246\254\355\214\251\355\206\240\353\247\201.md" index 9a378237..c4706b34 100644 --- "a/.github/ISSUE_TEMPLATE/\353\246\254\355\214\251\355\206\240\353\247\201.md" +++ "b/.github/ISSUE_TEMPLATE/\353\246\254\355\214\251\355\206\240\353\247\201.md" @@ -11,14 +11,17 @@ assignees: ''
-## 🏷️ 도메인 (해당하는 것에 체크) -- [ ] 📝 domain:post (게시글) -- [ ] 👤 domain:user (사용자) -- [ ] 🏢 domain:source (테크블로그 출처) -- [ ] 🔍 domain:search (검색) -- [ ] 🔔 domain:notification (알림) -- [ ] 📊 domain:recommendation (추천) -- [ ] 🌐 infra (인프라/배포) +## 🏷️ 도메인 (해당하는 것에 체크) +- [ ] 📝 domain:post (게시글) +- [ ] 👤 domain:useraccount (사용자 계정) +- [ ] 🧠 domain:personalization (개인화 프로필) +- [ ] 🏢 domain:source (테크블로그 출처) +- [ ] 🔍 domain:search (검색) +- [ ] 🔔 domain:notification (알림) +- [ ] 📊 domain:recommendation (추천) +- [ ] 🎯 domain:activity (사용자 활동) +- [ ] 🔐 domain:auth (인증/보안) +- [ ] 🌐 infra (인프라/배포)
@@ -39,4 +42,4 @@ assignees: '' - [ ] 코드 중복 제거 - [ ] 기타: -
\ No newline at end of file +
diff --git "a/.github/ISSUE_TEMPLATE/\353\262\204\352\267\270-\354\210\230\354\240\225.md" "b/.github/ISSUE_TEMPLATE/\353\262\204\352\267\270-\354\210\230\354\240\225.md" index ca9f6198..d9b08443 100644 --- "a/.github/ISSUE_TEMPLATE/\353\262\204\352\267\270-\354\210\230\354\240\225.md" +++ "b/.github/ISSUE_TEMPLATE/\353\262\204\352\267\270-\354\210\230\354\240\225.md" @@ -12,14 +12,17 @@ assignees: ''
-## 🏷️ 도메인 (해당하는 것에 체크) -- [ ] 📝 domain:post (게시글) -- [ ] 👤 domain:user (사용자) -- [ ] 🏢 domain:source (테크블로그 출처) -- [ ] 🔍 domain:search (검색) -- [ ] 🔔 domain:notification (알림) -- [ ] 📊 domain:recommendation (추천) -- [ ] 🌐 infra (인프라/배포) +## 🏷️ 도메인 (해당하는 것에 체크) +- [ ] 📝 domain:post (게시글) +- [ ] 👤 domain:useraccount (사용자 계정) +- [ ] 🧠 domain:personalization (개인화 프로필) +- [ ] 🏢 domain:source (테크블로그 출처) +- [ ] 🔍 domain:search (검색) +- [ ] 🔔 domain:notification (알림) +- [ ] 📊 domain:recommendation (추천) +- [ ] 🎯 domain:activity (사용자 활동) +- [ ] 🔐 domain:auth (인증/보안) +- [ ] 🌐 infra (인프라/배포)
@@ -34,4 +37,4 @@ assignees: '' ## 🎯 예상 원인 -
\ No newline at end of file +
diff --git "a/.github/ISSUE_TEMPLATE/\355\205\214\354\212\244\355\212\270-\354\236\221\354\204\261.md" "b/.github/ISSUE_TEMPLATE/\355\205\214\354\212\244\355\212\270-\354\236\221\354\204\261.md" index 0a452618..54ef1375 100644 --- "a/.github/ISSUE_TEMPLATE/\355\205\214\354\212\244\355\212\270-\354\236\221\354\204\261.md" +++ "b/.github/ISSUE_TEMPLATE/\355\205\214\354\212\244\355\212\270-\354\236\221\354\204\261.md" @@ -16,7 +16,8 @@ assignees: '' ## 🏷️ 도메인 (해당하는 것에 체크) - [ ] 📝 domain:post (게시글) -- [ ] 👤 domain:user (사용자) +- [ ] 👤 domain:useraccount (사용자 계정) +- [ ] 🧠 domain:personalization (개인화 프로필) - [ ] 🏢 domain:source (테크블로그 출처) - [ ] 🔍 domain:search (검색) - [ ] 🔔 domain:notification (알림)