Skip to content

[feat] 사용자 검색 api 구현#113

Merged
buzz0331 merged 14 commits intodevelopfrom
feat/#107-search-user
Jul 30, 2025
Merged

[feat] 사용자 검색 api 구현#113
buzz0331 merged 14 commits intodevelopfrom
feat/#107-search-user

Conversation

@buzz0331
Copy link
Contributor

@buzz0331 buzz0331 commented Jul 30, 2025

#️⃣ 연관된 이슈

closes #107

📝 작업 내용

정확도순 구현을 위해 다음과 같은 방식을 사용했습니다.

  1. 가중치 기반 정렬
  • 검색 키워드와 완전히 일치시 -> 가중치 3
  • 검색 키워드를 접두어로 포함하고 있는 닉네임 -> 가중치 2
  • 검색 키워드를 사이에 포함하고 있는 닉네임 -> 가중치 1
  1. 가중치 일치시 사전순 정렬
  • 가중치가 일치할 경우, 길이 순으로 오름차순 정렬 + 사전순 정렬할지 아니면 사전순으로 정렬할지를 고민했는데, @heeeeyong 님께 여쭤본 결과, 가중치가 일치하면 사전순으로만 정렬해도 괜찮을 것 같다고 판단했습니다!

RecentSearchManager 구현

  • 최근검색어 추가를 담당하는 헬퍼 서비스입니다.
  • 같은 사용자가 같은 searchType(사용자 검색, 책 검색)으로 같은 검색을 한 최근 검색이 존재할 경우 createdAt만 업데이트 해줍니다.
  • 존재하지 않을 경우, 새로운 RecentSearch를 추가합니다.
  • 삭제에 대한건 스케줄러를 돌려서 해결하기로 하였습니다! [THIP2025-166] [feat] 스케줄러 필요한 기능 #111

📸 스크린샷

💬 리뷰 요구사항

기존에 구현했던 팔로우 관련 조회 api에서 사용됐던 FollowQueryDto과 이번 api에서도 반환값이 비슷한 것 같아서 UserQueryDto로 통합하여 재사용하였습니다. 리뷰할때 참고바랍니다!

📌 PR 진행 시 이러한 점들을 참고해 주세요

* P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
* P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
* P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)

Summary by CodeRabbit

  • 신규 기능

    • 사용자 검색 API가 추가되어 닉네임으로 사용자를 검색할 수 있습니다.
    • 최근 검색어 자동 저장, 갱신, 삭제 및 수정 기능이 도입되었습니다.
  • 기능 개선

    • 팔로워/팔로잉 목록에 별명 색상(aliasColor) 정보가 추가되었습니다.
    • 팔로워/팔로잉 및 검색 결과 응답 구조가 개선되고, 사용자 검색 결과가 정확도 기준으로 정렬됩니다.
  • 버그 수정

    • 최근 검색어 관련 오류 코드 및 예외 처리가 추가되었습니다.
  • 테스트

    • 사용자 검색 및 최근 검색어 저장에 대한 통합 테스트가 추가되었습니다.

@coderabbitai
Copy link

coderabbitai bot commented Jul 30, 2025

"""

Walkthrough

이번 변경에서는 사용자 검색 API를 신규로 개발하고, 최근 검색어 저장 및 관련 도메인/포트/어댑터 계층을 확장하였습니다. 사용자 검색 결과에 aliasColor 등 추가 정보를 포함하도록 DTO 및 매퍼 구조가 변경되었고, 최근 검색어 중복 시 갱신 로직과 예외 코드가 추가되었습니다. 통합 테스트가 도입되었습니다.

Changes

Cohort / File(s) Change Summary
최근검색 저장/조회 기능 확장
src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java, .../RecentSearchQueryPersistenceAdapter.java, .../RecentSearchJpaRepository.java, .../RecentSearchQueryRepository.java, .../RecentSearchQueryRepositoryImpl.java, .../RecentSearchJpaEntity.java, .../SearchType.java, .../application/port/out/RecentSearchCommandPort.java, .../RecentSearchQueryPort.java, .../domain/RecentSearch.java, .../application/service/manager/RecentSearchCreateManager.java, src/main/java/konkuk/thip/common/exception/code/ErrorCode.java, src/main/java/konkuk/thip/common/entity/BaseDomainEntity.java
최근검색 저장 메서드 시그니처 변경, 삭제/수정/중복 갱신 로직 및 예외 추가, JPA/QueryDSL 기반 쿼리 리포지토리 구현, 엔티티/도메인/포트/매니저 계층 확장 및 관련 유틸리티 메서드 추가.
사용자 검색 API 도입
src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java, .../response/UserSearchResponse.java, .../application/port/in/UserSearchUsecase.java, .../application/port/in/dto/UserSearchQuery.java, .../application/service/UserSearchService.java, .../application/mapper/UserQueryMapper.java
사용자 검색 API 엔드포인트 및 서비스/유스케이스/쿼리 DTO/응답 DTO/매퍼 신규 도입. 검색 시 최근검색 저장 연동.
팔로우/팔로잉/검색 DTO 및 매퍼 구조 개선
.../adapter/in/web/response/UserFollowersResponse.java, .../UserFollowingResponse.java, .../application/port/out/FollowingQueryPort.java, .../application/port/out/dto/UserQueryDto.java, .../adapter/out/persistence/FollowingQueryPersistenceAdapter.java, .../repository/following/FollowingQueryRepository.java, .../FollowingQueryRepositoryImpl.java, .../mapper/FollowQueryMapper.java, .../service/following/UserGetFollowService.java
팔로워/팔로잉/검색 DTO를 FollowQueryDto → UserQueryDto로 일원화, aliasColor 필드 추가, 관련 매퍼 및 서비스/포트/리포지토리 시그니처/타입 일괄 변경.
사용자 검색 쿼리/리포지토리 확장
.../adapter/out/persistence/UserQueryPersistenceAdapter.java, .../repository/UserQueryRepository.java, .../repository/UserQueryRepositoryImpl.java, .../application/port/out/UserQueryPort.java
닉네임 기반 사용자 검색 쿼리 및 정확도 정렬 로직 추가, 관련 리포지토리/포트/어댑터 확장.
테스트 추가
src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java
사용자 검색 및 최근검색 저장 통합 테스트 신규 작성.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant UserQueryController
    participant UserSearchService
    participant UserQueryPort
    participant UserQueryMapper
    participant RecentSearchCreateManager

    Client->>UserQueryController: GET /users?keyword=...
    UserQueryController->>UserSearchService: searchUsers(UserSearchQuery)
    UserSearchService->>UserQueryPort: findUsersByNicknameOrderByAccuracy(keyword, userId, size)
    UserQueryPort-->>UserSearchService: List<UserQueryDto>
    UserSearchService->>UserQueryMapper: toUserDtoList(List<UserQueryDto>)
    UserQueryMapper-->>UserSearchService: List<UserSearchResponse.UserDto>
    UserSearchService->>RecentSearchCreateManager: saveRecentSearchByUser(userId, keyword)
    RecentSearchCreateManager->>RecentSearchQueryPort: findRecentSearchByKeywordAndUserId(keyword, userId)
    alt 이미 존재
        RecentSearchCreateManager->>RecentSearchCommandPort: update(RecentSearch)
    else 신규
        RecentSearchCreateManager->>RecentSearchCommandPort: save(RecentSearch)
    end
    UserSearchService-->>UserQueryController: UserSearchResponse
    UserQueryController-->>Client: BaseResponse<UserSearchResponse>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Assessment against linked issues

Objective Addressed Explanation
사용자 검색 조회 API 신규 개발 (#107)
사용자 검색 시 최근 검색어 저장 (#107)
검색 결과에 aliasColor 등 추가 정보 제공 (#107)
중복 검색어 입력 시 createdAt 갱신 (#107)

Assessment against linked issues: Out-of-scope changes

(해당 변경에서 명확히 이슈 범위를 벗어난 기능적 변경은 발견되지 않았습니다.)

Possibly related PRs

Suggested labels

🐶 희진

Suggested reviewers

  • hd0rable
  • seongjunnoh

Poem

🐇
사용자 검색, 토끼가 달려
aliasColor로 무지개 펼쳐
최근 검색도 잊지 않고
중복이면 시간만 바꿔주고
테스트까지 꼼꼼하게
오늘도 코드는 한 뼘 더 성장해!
🥕✨
"""

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b9f378 and 46adbec.

📒 Files selected for processing (1)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#107-search-user

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (7)
src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java (1)

15-15: 사용자 검색 메서드가 적절히 추가되었습니다.

CQRS 패턴의 QueryPort에 맞게 조회 전용 메서드가 잘 추가되었습니다. 메서드명도 의도를 명확히 표현하고 있습니다.

매개변수 validation 강화를 권장합니다:

  • keyword의 null/blank 체크
  • size의 양수 및 최대값 제한
  • userId의 null 체크
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepository.java (1)

11-11: 메서드 설계가 적절함

메서드명이 명확하고 파라미터 구성이 논리적입니다. 정확도 기반 정렬이라는 비즈니스 요구사항이 메서드명에 잘 반영되어 있습니다.

Integer size 파라미터의 null 안전성을 고려하여 다음 중 하나를 적용하는 것을 권장합니다:

-    List<UserQueryDto> findUsersByNicknameOrderByAccuracy(String keyword, Long userId, Integer size);
+    List<UserQueryDto> findUsersByNicknameOrderByAccuracy(String keyword, Long userId, int size);

또는 호출부에서 적절한 기본값 검증을 추가해주세요.

src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java (1)

80-84: 검색 결과 순서에 대한 명시적 검증 추가

현재 테스트는 닉네임 순서를 검증하지만, 왜 이 순서인지에 대한 설명이 부족합니다. 가중치 기반 정렬 로직을 명확히 하기 위해 주석을 추가하는 것을 권장합니다.

         // then: 검색 결과 검증
+        // 가중치 기반 정렬: thipalpha, thipbeta, thipgamma (시작=2), 123thip (포함=1)
         result.andExpect(status().isOk())
                 .andExpect(jsonPath("$.data.userList", hasSize(4)))
                 .andExpect(jsonPath("$.data.userList[0].nickname").value("thipalpha"))
src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepositoryImpl.java (1)

26-26: 쿼리 성능 최적화 고려사항

현재 쿼리는 userJpaEntity.userId를 통해 조인을 수행합니다. 자주 사용되는 쿼리라면 (searchTerm, type, userId) 복합 인덱스 추가를 고려해보세요.

데이터베이스 스키마에 다음과 같은 인덱스 추가를 권장합니다:

CREATE INDEX idx_recent_search_term_type_user ON recent_search (search_term, type, user_id);
src/main/java/konkuk/thip/user/application/service/UserSearchService.java (1)

23-23: 트랜잭션 범위 검토 필요

최근 검색어 저장을 위한 트랜잭션 적용은 적절하나, 사용자 검색 조회 자체는 읽기 전용 작업입니다. @Transactional(readOnly = true)를 기본으로 하고, 최근 검색어 저장 부분만 별도 트랜잭션으로 처리하는 것을 고려해보세요.

@Override
-@Transactional // <- 최근 검색 저장으로 인한 트랜잭션
+@Transactional(readOnly = true)
public UserSearchResponse searchUsers(UserSearchQuery userSearchQuery) {
    var userDtoList = userQueryMapper.toUserDtoList(userQueryPort.findUsersByNicknameOrderByAccuracy(
                    userSearchQuery.keyword().toLowerCase(),
                    userSearchQuery.userId(),
                    userSearchQuery.size()
            ));

    // 최근 검색어 저장
-   recentSearchCreateManager.saveRecentSearchByUser(userSearchQuery.userId(), userSearchQuery.keyword());
+   recentSearchCreateManager.saveRecentSearchByUser(userSearchQuery.userId(), userSearchQuery.keyword());

    return UserSearchResponse.of(userDtoList);
}

그리고 RecentSearchCreateManager.saveRecentSearchByUser 메서드에 @Transactional 어노테이션을 적용하세요.

src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java (1)

42-71: 사용자 검색 로직이 잘 구현되었습니다

가중치 기반 검색 로직이 요구사항에 맞게 정확히 구현되었습니다:

  • 정확한 매칭: 가중치 3
  • 접두사 매칭: 가중치 2
  • 부분 매칭: 가중치 1
  • 우선순위 내림차순, 닉네임 오름차순으로 정렬

다만 몇 가지 개선사항을 제안합니다.

개선 제안:

  1. 성능 최적화: LIKE 연산자 사용 시 인덱스 활용을 위해 nickname 컬럼에 인덱스가 있는지 확인해주세요.

  2. 코드 가독성: CaseBuilder 로직을 별도 메서드로 분리하면 가독성이 향상됩니다:

+ private NumberExpression<Integer> createSearchPriority(QUserJpaEntity user, String keyword) {
+     String exactPattern = keyword;
+     String prefixPattern = keyword + "%";
+     String containsPattern = "%" + keyword + "%";
+     
+     return new CaseBuilder()
+             .when(user.nickname.eq(exactPattern)).then(3)
+             .when(user.nickname.like(prefixPattern)).then(2)
+             .when(user.nickname.like(containsPattern)).then(1)
+             .otherwise(0);
+ }

- NumberExpression<Integer> priority = new CaseBuilder()
-         .when(user.nickname.eq(keyword)).then(3)
-         .when(user.nickname.like(keyword + "%")).then(2)
-         .when(user.nickname.like(pattern)).then(1)
-         .otherwise(0);
+ NumberExpression<Integer> priority = createSearchPriority(user, keyword);
src/main/java/konkuk/thip/user/application/port/in/dto/UserSearchQuery.java (1)

8-10: 경미: of 팩토리 메서드가 현재는 불필요하게 보입니다.

입력 검증을 Canonical Constructor로 이전하면 of는 단순 위임만 수행하게 되어 중복될 가능성이 큽니다.
통일성을 위해 유지할 수도 있으나, 불필요한 계층을 줄이고 싶다면 삭제를 고려해 주세요.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2b62cf and b42806f.

📒 Files selected for processing (34)
  • src/main/java/konkuk/thip/book/application/service/BookSearchService.java (1 hunks)
  • src/main/java/konkuk/thip/common/entity/BaseDomainEntity.java (2 hunks)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1 hunks)
  • src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchJpaEntity.java (2 hunks)
  • src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/SearchType.java (1 hunks)
  • src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java (2 hunks)
  • src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchQueryPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepository.java (1 hunks)
  • src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepositoryImpl.java (1 hunks)
  • src/main/java/konkuk/thip/recentSearch/application/port/out/RecentSearchCommandPort.java (1 hunks)
  • src/main/java/konkuk/thip/recentSearch/application/port/out/RecentSearchQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java (1 hunks)
  • src/main/java/konkuk/thip/recentSearch/domain/RecentSearch.java (2 hunks)
  • src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java (2 hunks)
  • src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowingResponse.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/in/web/response/UserSearchResponse.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java (3 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java (2 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepository.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java (2 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java (2 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java (5 hunks)
  • src/main/java/konkuk/thip/user/application/mapper/FollowQueryMapper.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/mapper/UserQueryMapper.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/port/in/UserSearchUsecase.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/port/in/dto/UserSearchQuery.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/port/out/dto/UserQueryDto.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/service/UserSearchService.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java (3 hunks)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java (1 hunks)
🧰 Additional context used
🧠 Learnings (22)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/book/application/service/BookSearchService.java (1)

Learnt from: seongjunnoh
PR: #43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

src/main/java/konkuk/thip/recentSearch/application/port/out/RecentSearchQueryPort.java (1)

Learnt from: seongjunnoh
PR: #43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

src/main/java/konkuk/thip/user/application/port/in/dto/UserSearchQuery.java (1)

Learnt from: seongjunnoh
PR: #43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchQueryPersistenceAdapter.java (1)

Learnt from: seongjunnoh
PR: #43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchJpaEntity.java (1)

Learnt from: seongjunnoh
PR: #36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.

src/main/java/konkuk/thip/user/application/mapper/UserQueryMapper.java (2)

Learnt from: seongjunnoh
PR: #43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

Learnt from: buzz0331
PR: #78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.

src/main/java/konkuk/thip/user/application/port/in/UserSearchUsecase.java (1)

Learnt from: seongjunnoh
PR: #43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java (2)

Learnt from: seongjunnoh
PR: #43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

Learnt from: buzz0331
PR: #78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.

src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java (1)

Learnt from: seongjunnoh
PR: #43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java (1)

Learnt from: seongjunnoh
PR: #43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java (1)

Learnt from: seongjunnoh
PR: #43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

src/main/java/konkuk/thip/user/adapter/in/web/response/UserSearchResponse.java (1)

Learnt from: buzz0331
PR: #78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.

src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java (2)

Learnt from: buzz0331
PR: #78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.

Learnt from: seongjunnoh
PR: #43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

src/main/java/konkuk/thip/recentSearch/application/port/out/RecentSearchCommandPort.java (1)

Learnt from: seongjunnoh
PR: #43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepositoryImpl.java (1)

Learnt from: seongjunnoh
PR: #36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.

src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java (1)

Learnt from: seongjunnoh
PR: #36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.

src/main/java/konkuk/thip/user/application/mapper/FollowQueryMapper.java (1)

Learnt from: buzz0331
PR: #78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.

src/main/java/konkuk/thip/user/application/service/UserSearchService.java (1)

Learnt from: seongjunnoh
PR: #43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java (1)

Learnt from: seongjunnoh
PR: #43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepository.java (1)

Learnt from: seongjunnoh
PR: #36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.

src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java (2)

Learnt from: buzz0331
PR: #78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.

Learnt from: seongjunnoh
PR: #43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

🧬 Code Graph Analysis (4)
src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (29-293)
src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepositoryImpl.java (2)
src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchQueryPersistenceAdapter.java (1)
  • Repository (14-26)
src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java (1)
  • Repository (17-54)
src/main/java/konkuk/thip/user/application/mapper/FollowQueryMapper.java (1)
src/main/java/konkuk/thip/user/application/mapper/FollowDtoMapper.java (1)
  • FollowDtoMapper (8-14)
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java (1)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)
  • DateUtil (12-62)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (46)
src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/SearchType.java (1)

13-13: DB 호환성 확인 필요

  • SearchType.BOOK_SEARCH의 문자열 값이 "책_검색"에서 "책 검색"으로 변경되었습니다.
  • 기존 DB의 recent_search 테이블 type 컬럼에 여전히 "책_검색" 값이 남아 있는지 확인해주세요.
  • 필요 시 아래 중 하나를 적용해 주세요:
    • 데이터 마이그레이션(SQL 스크립트)으로 기존 값을 "책 검색"으로 업데이트
    • JPA AttributeConverter 또는 로직 수정으로 "책_검색"도 정상 매핑되도록 하위 호환성 확보
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1)

75-75: 적절한 에러 코드 추가입니다.

최근 검색 기능의 삭제/업데이트 작업에서 사용할 에러 코드가 적절히 추가되었습니다. 90000번대 recentSearch 에러 범위에 맞게 분류되어 있고 메시지도 명확합니다.

src/main/java/konkuk/thip/book/application/service/BookSearchService.java (1)

75-75: 메서드 호출 방식 개선이 적절합니다.

recentSearchCommandPort.save() 호출에서 userId 매개변수가 제거되어 더 간결해졌습니다. RecentSearch 객체에 userId가 포함되어 있어 별도 전달이 불필요하므로 응집도가 향상되었습니다.

src/main/java/konkuk/thip/user/application/port/in/UserSearchUsecase.java (2)

6-8: UseCase 인터페이스 구조는 적절함

단일 책임 원칙을 잘 따르고 있으며, 메서드 시그니처가 명확합니다.


3-3: 아키텍처 위반: 애플리케이션 레이어가 어댑터 레이어에 의존

UseCase 인터페이스가 웹 어댑터 레이어의 UserSearchResponse를 import하고 있습니다. 클린 아키텍처에서 애플리케이션 레이어는 어댑터 레이어에 의존해서는 안 됩니다.

다음과 같이 수정을 제안합니다:

-import konkuk.thip.user.adapter.in.web.response.UserSearchResponse;
+import konkuk.thip.user.application.port.in.dto.UserSearchResponse;

UserSearchResponsesrc/main/java/konkuk/thip/user/application/port/in/dto/ 패키지로 이동하거나, 별도의 application layer DTO를 생성하여 의존성 방향을 올바르게 유지해주세요.

⛔ Skipped due to learnings
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/recentSearch/application/port/out/RecentSearchQueryPort.java (1)

9-9: CQRS 패턴을 잘 준수한 QueryPort 메서드

메서드 시그니처가 적절하며, Optional<RecentSearch> 반환 타입으로 null 안전성을 보장하고 있습니다. 검색된 학습 내용에 따르면 QueryPort에는 도메인 엔티티를 조회하는 메서드를 추가하는 것이 이 프로젝트의 컨벤션과 일치합니다.

src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchJpaEntity.java (1)

34-38: createdAt 필드 업데이트 로직 검토 필요

updateFrom 메서드에서 createdAt 필드를 업데이트하고 있습니다. 일반적으로 createdAt은 생성 시점을 나타내므로 업데이트 시에는 변경하지 않고, updatedAt 필드를 수정하는 것이 일반적입니다.

이것이 의도된 비즈니스 로직인지 확인해주세요. 만약 최근 검색어의 타임스탬프를 갱신하는 것이 요구사항이라면, 필드명을 lastSearchedAt 같은 더 명확한 이름으로 변경하는 것을 고려해보세요.

-        this.setCreatedAt(recentSearch.getCreatedAt());
+        // 비즈니스 요구사항에 따라 적절한 필드 업데이트
+        this.setUpdatedAt(LocalDateTime.now()); // 또는
+        this.setLastSearchedAt(recentSearch.getLastSearchedAt());
src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchJpaRepository.java (1)

5-8: LGTM! 표준 Spring Data JPA 패턴을 잘 따르고 있습니다.

@Repository 어노테이션 추가와 RecentSearchQueryRepository 확장이 적절하게 구현되었습니다. 커스텀 쿼리 기능을 위한 다중 상속 패턴이 올바르게 적용되었습니다.

src/main/java/konkuk/thip/user/application/mapper/UserQueryMapper.java (1)

9-13: LGTM! MapStruct 매퍼가 올바르게 구현되었습니다.

Spring 컴포넌트 모델 설정과 리스트 매핑 메서드 구현이 적절합니다. 학습된 내용에 따르면 THIP 프로젝트에서는 Query API에 한해 application 계층에서 response DTO를 직접 참조하는 것이 허용되므로 현재 구현이 팀 컨벤션에 부합합니다.

src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java (1)

36-39: LGTM! CQRS QueryPort 패턴을 올바르게 구현했습니다.

새로운 findUsersByNicknameOrderByAccuracy 메서드가 적절하게 구현되었습니다. 학습된 내용에 따르면 QueryPort는 조회 API의 response에 해당하는 데이터를 DB로부터 조회하는 메서드를 포함해야 하며, 현재 구현이 이 컨벤션을 잘 따르고 있습니다.

src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepository.java (1)

8-10: LGTM! 쿼리 리포지토리 인터페이스가 잘 설계되었습니다.

메서드명이 Spring Data JPA 컨벤션을 따르고 있으며, Optional 반환 타입을 사용하여 null 안전성을 확보했습니다. 학습된 내용에 따르면 이는 권장되는 패턴입니다.

src/main/java/konkuk/thip/recentSearch/domain/RecentSearch.java (1)

21-27: LGTM! 정적 팩토리 메서드가 잘 구현되었습니다.

withoutId 정적 팩토리 메서드가 적절하게 구현되어 새로운 엔티티 생성을 위한 깔끔한 API를 제공합니다. 빌더 패턴 사용도 적절합니다.

src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowingResponse.java (1)

9-9: DTO 통합 및 필드 추가가 적절합니다.

Following에서 FollowingDto로의 네이밍 변경과 aliasColor 필드 추가가 PR 목표에 부합하며, DTO 통합 작업의 일환으로 적절하게 구현되었습니다.

Also applies to: 14-20

src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java (1)

9-9: 일관된 DTO 리팩토링이 잘 적용되었습니다.

UserFollowingResponse와 동일한 패턴으로 FollowerFollowerDto로 변경하고 aliasColor 필드를 추가한 것이 일관성 있고 적절합니다.

Also applies to: 14-20

src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchQueryPersistenceAdapter.java (2)

6-6: 필요한 imports가 적절하게 추가되었습니다.

도메인 엔티티 RecentSearch, Optional, 그리고 USER_SEARCH 상수의 static import가 새로운 메서드 구현에 필요한 의존성들을 명확하게 추가했습니다.

Also applies to: 10-10, 12-12


21-25: 최근 검색어 조회 메서드가 올바르게 구현되었습니다.

CQRS 패턴에 따라 QueryPort 인터페이스를 구현하며, 리포지토리 호출 후 매퍼를 통한 도메인 엔티티 변환이 적절하게 처리되었습니다. Optional 반환 타입도 데이터 존재 여부를 안전하게 처리할 수 있어 좋습니다.

src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java (3)

5-5: 검증 어노테이션과 imports가 적절하게 추가되었습니다.

새로운 검색 API를 위한 @NotBlank 검증 어노테이션, @Validated 클래스 레벨 어노테이션, 그리고 관련 imports가 올바르게 추가되었습니다.

Also applies to: 15-15, 21-21


8-8: 새로운 검색 기능을 위한 의존성이 적절하게 추가되었습니다.

wildcard import 사용과 UserSearchUsecase, UserSearchQuery 등 검색 기능에 필요한 의존성들이 명확하게 정의되었습니다.

Also applies to: 11-11, 13-13, 29-29


71-76: 사용자 검색 API가 잘 구현되었습니다.

검색어 필수 검증(@NotBlank), 사이즈 제한(1-30), 그리고 기본값 설정이 적절하며, PR 목표에 부합하는 가중치 기반 정확도 순 검색 기능이 올바르게 구현되었습니다.

src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java (3)

7-7: DTO 통합을 위한 import 변경이 적절합니다.

FollowQueryDto에서 UserQueryDto로의 변경이 PR 목표인 DTO 통합 작업에 부합합니다.


32-34: 팔로워 조회 로직의 DTO 타입 변경이 일관되게 적용되었습니다.

CursorBasedList<UserQueryDto> 타입 변경과 toFollowerDto 매퍼 메서드 호출이 DTO 통합 리팩토링에 맞게 올바르게 수정되었습니다.

Also applies to: 37-37


52-54: 팔로잉 조회 로직의 DTO 타입 변경이 일관되게 적용되었습니다.

팔로워 조회와 동일한 패턴으로 UserQueryDto 타입과 toFollowingDto 매퍼 메서드가 일관성 있게 적용되었습니다.

Also applies to: 57-57

src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java (1)

87-93: 최근 검색어 저장 검증 로직 승인

최근 검색어가 올바르게 저장되는지 확인하는 검증 로직이 잘 구현되어 있습니다. 키워드, 검색 타입, 사용자 ID 모두 올바르게 검증하고 있습니다.

src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepositoryImpl.java (1)

20-31: QueryDSL 구현이 잘 작성되었습니다

QueryDSL을 사용한 커스텀 쿼리 구현이 깔끔하고 타입 안전성을 보장합니다. Optional 반환 타입 사용으로 null 안전성도 확보되었습니다.

src/main/java/konkuk/thip/user/application/port/out/dto/UserQueryDto.java (2)

8-14: DTO 필드 추가 및 이름 변경이 적절합니다

FollowQueryDto에서 UserQueryDto로의 이름 변경과 aliasColor 필드 추가가 사용자 검색 기능 확장에 적합합니다. QueryProjection 어노테이션도 올바르게 적용되었습니다.


22-22: Validation 일관성 확인

aliasColor 필드에 대한 null 검증이 추가되었습니다. 다른 필드들과 일관된 검증 방식을 사용하고 있어 좋습니다.

src/main/java/konkuk/thip/user/adapter/in/web/response/UserSearchResponse.java (2)

5-21: Response DTO 구조가 잘 설계되었습니다

Java Record를 활용한 깔끔한 응답 DTO 구조입니다. 중첩된 UserDto 레코드와 정적 팩토리 메서드 of를 통해 가독성과 사용성이 좋습니다.


8-16: UserDto 필드 구성 확인

사용자 검색 결과에 필요한 모든 필드가 포함되어 있고, 새로 추가된 aliasColor 필드도 적절히 반영되었습니다. 필드명도 명확하고 일관성 있습니다.

src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java (2)

23-35: Optional.ifPresentOrElse 사용이 적절합니다

조건부 로직을 Optional.ifPresentOrElse로 깔끔하게 처리한 것이 좋습니다. 코드 가독성과 함수형 프로그래밍 스타일이 잘 적용되었습니다.


27-27: RecentSearchCreateManager — createdAt 업데이트 의도 검토 필요

  • 위치: src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java:27
  • BaseDomainEntity에 createdAt·modifiedAt 필드가 선언되어 있고, RecentSearch 엔티티에는 updateCreatedAt(LocalDateTime)만 제공되어 있습니다.
  • 현 로직은 기존 검색 기록이 있을 때 createdAt을 덮어써서 정렬 순서를 제어하는 방식으로 보입니다.

도메인 요구사항을 다시 한번 확인해주세요:

  1. 검색 기록 재사용 시 createdAt을 재설정하는 것이 실제 의도된 동작인지
  2. 최근 검색어 정렬 기준으로 createdAt을 사용하고 있는지

필드 명의 명확성을 위해 lastSearchedAt 또는 updatedAt 같은 별도 필드 도입을 고려해 보시기 바랍니다.

src/main/java/konkuk/thip/user/application/service/UserSearchService.java (2)

25-29: LGTM! 검색 로직이 잘 구현되었습니다

키워드를 소문자로 변환하여 대소문자 구분 없는 검색을 지원하고, 정확도 기준 정렬을 적절히 활용하고 있습니다.


31-32: 최근 검색어 저장 시점 적절

검색 결과 조회 후 최근 검색어를 저장하는 순서가 적절합니다. 검색이 실패하면 최근 검색어도 저장되지 않아 일관성이 보장됩니다.

src/main/java/konkuk/thip/recentSearch/application/port/out/RecentSearchCommandPort.java (1)

7-10: LGTM! CQRS 패턴에 맞는 인터페이스 설계

CommandPort 인터페이스가 적절히 설계되었습니다:

  • save 메서드가 도메인 객체만 받도록 단순화됨
  • CRUD 작업을 위한 delete, update 메서드 추가
  • 메서드 시그니처가 도메인 중심적으로 설계됨

학습된 THIP 프로젝트의 CQRS 컨벤션에도 잘 부합합니다.

src/main/java/konkuk/thip/user/application/mapper/FollowQueryMapper.java (2)

5-5: DTO 통합이 적절히 반영됨

FollowQueryDtoUserQueryDto로 통합하는 PR 목표가 잘 반영되었습니다.


11-13: FollowDtoMapper 없음으로 중복 검토 불필요
FollowDtoMapper.java 파일이 레포지토리에 존재하지 않습니다. 현재는 FollowQueryMapper만 정의·사용 중이므로 중복 우려가 없습니다. 이전 리뷰 코멘트는 무시해도 좋습니다.

Likely an incorrect or invalid review comment.

src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java (1)

4-8: DTO 통합이 일관성 있게 적용됨

FollowQueryDto에서 UserQueryDto로의 변경이 QueryPort 인터페이스에 일관성 있게 적용되었습니다. CQRS 패턴의 QueryPort 컨벤션에도 부합합니다.

src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java (2)

4-4: DTO 통합이 repository 계층까지 일관성 있게 전파됨

UserQueryDto import 변경이 적절하며, 전체 계층에서 일관성 있는 DTO 통합이 이루어졌습니다.


13-14: 메서드 시그니처 업데이트 적절

반환 타입이 UserQueryDto로 올바르게 변경되었으며, 메서드명도 여전히 의미가 명확합니다.

src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java (1)

23-23: 필드명 변경이 일관성 있게 적용되었는지 확인 필요

jpaQueryFactory에서 queryFactory로 필드명이 변경되었습니다. 변경된 필드명이 메서드 내에서 일관성 있게 사용되고 있는지 확인해주세요.

src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java (2)

27-36: 메서드 시그니처 개선이 잘 적용되었습니다

RecentSearch 객체에서 userId를 직접 추출하여 사용하는 방식으로 개선되어 캡슐화가 향상되었습니다.


46-53: update 메서드 로직이 적절합니다

엔티티 조회 후 updateFrom 메서드를 사용하여 업데이트하는 방식이 깔끔하게 구현되었습니다. 예외 처리도 일관성 있게 적용되었습니다.

src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java (2)

5-5: DTO 통합이 일관성 있게 적용되었습니다

FollowQueryDto에서 UserQueryDto로의 변경이 PR 목표에 맞게 잘 적용되었습니다.


21-42: 메서드 반환 타입 변경이 적절합니다

두 메서드 모두 UserQueryDto를 사용하도록 일관성 있게 변경되었으며, 기존 로직은 그대로 유지되어 안전한 리팩토링입니다.

src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java (3)

10-11: DTO 클래스 변경이 일관성 있게 적용되었습니다

QFollowQueryDto에서 QUserQueryDto로의 변경이 코드베이스 전반에 걸쳐 일관성 있게 적용되었습니다.


40-56: 메서드 시그니처 업데이트가 적절합니다

팔로워와 팔로잉 조회 메서드의 반환 타입이 UserQueryDto로 일관성 있게 변경되었습니다.


75-83: 쿼리 프로젝션에 aliasColor 필드 추가가 적절합니다

alias.color 필드가 프로젝션에 추가되어 사용자 별칭 색상 정보를 포함하도록 개선되었습니다. 이는 통합된 UserQueryDto의 새로운 필드와 일치합니다.

Comment on lines +13 to 14
@Setter
private LocalDateTime createdAt;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

createdAt 필드 수정 가능성에 대한 신중한 검토가 필요합니다.

Base 엔티티의 createdAt 필드에 @Setter를 추가한 것은 일반적인 도메인 규칙에 어긋날 수 있습니다. 생성 시간은 보통 불변이어야 하는데, 최근 검색 기능에서 "최근성" 업데이트를 위해 수정하는 것이 적절한지 검토가 필요합니다.

대안으로 다음을 고려해보세요:

  1. 최근 검색에서는 기존 레코드를 삭제하고 새로 생성
  2. 별도의 lastSearchedAt 필드 추가
  3. RecentSearch만을 위한 별도 메서드 제공
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/entity/BaseDomainEntity.java around lines 13
to 14, the createdAt field currently has a @Setter which allows modification,
but createdAt should be immutable as it represents the creation timestamp.
Remove the @Setter annotation from createdAt to prevent external modification.
Instead, implement one of the suggested alternatives such as adding a separate
lastSearchedAt field or handling recent search updates by deleting and
recreating records, ensuring createdAt remains unchanged.

Comment on lines +38 to +44
@Override
public void delete(Long id) {
recentSearchJpaRepository.delete(
recentSearchJpaRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND))
);
}
Copy link

@coderabbitai coderabbitai bot Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

delete 메서드 구현을 개선해주세요

현재 구현에서 findByIddelete를 연쇄 호출하고 있는데, deleteById를 사용하면 더 효율적입니다.

다음과 같이 개선해주세요:

 @Override
 public void delete(Long id) {
-     recentSearchJpaRepository.delete(
-             recentSearchJpaRepository.findById(id)
-                     .orElseThrow(() -> new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND))
-     );
+     if (!recentSearchJpaRepository.existsById(id)) {
+         throw new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND);
+     }
+     recentSearchJpaRepository.deleteById(id);
 }
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java
around lines 38 to 44, the delete method currently calls findById followed by
delete, which is inefficient. Replace this by directly using
recentSearchJpaRepository.deleteById(id) to improve performance and simplify the
code.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 이런 구현 방식도 괜찮은거 같네요

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai 너가 제안한 delete 방식이 현재 구현된 방식보다 어떤 점에서 더 효율적인지 설명해줘

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seongjunnoh 좋은 질문이네요! 제가 제안한 방식이 더 효율적인 이유를 설명드리겠습니다.

현재 구현 방식 vs 제안된 방식

현재 구현 (비효율적):

// 1단계: SELECT 쿼리로 전체 엔티티 조회
RecentSearchJpaEntity entity = recentSearchJpaRepository.findById(id)
    .orElseThrow(() -> new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND));
// 2단계: DELETE 쿼리 실행
recentSearchJpaRepository.delete(entity);

제안된 구현 (효율적):

// 1단계: SELECT COUNT 쿼리로 존재 여부만 확인
if (!recentSearchJpaRepository.existsById(id)) {
    throw new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND);
}
// 2단계: DELETE 쿼리 실행
recentSearchJpaRepository.deleteById(id);

효율성 차이점

  1. 메모리 사용량:

    • 현재: 전체 엔티티 객체를 메모리에 로드
    • 제안: 존재 여부(boolean)만 확인
  2. 네트워크 트래픽:

    • 현재: 모든 컬럼 데이터를 DB에서 가져옴
    • 제안: 존재 여부만 확인 (훨씬 적은 데이터)
  3. 데이터베이스 연산:

    • 현재: SELECT * FROM recent_search WHERE id = ?
    • 제안: SELECT 1 FROM recent_search WHERE id = ? LIMIT 1
  4. JPA 최적화:

    • deleteById(id)는 JPA가 내부적으로 최적화된 DELETE 쿼리를 직접 실행
    • delete(entity)는 영속성 컨텍스트에서 엔티티 관리 오버헤드 발생

특히 대용량 데이터나 빈번한 삭제 작업이 있는 환경에서 이런 최적화가 누적되면 상당한 성능 개선 효과를 가져올 수 있습니다!


✏️ Learnings added
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.918Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seongjunnoh 제가 제안한 delete 방식이 더 효율적인 이유를 데이터베이스 쿼리와 메모리 관점에서 설명드리겠습니다.

현재 구현 방식의 문제점

// 현재 방식
recentSearchJpaRepository.delete(
    recentSearchJpaRepository.findById(id)
        .orElseThrow(() -> new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND))
);

실행되는 쿼리:

  1. SELECT * FROM recent_search WHERE id = ? - 전체 엔티티 조회
  2. DELETE FROM recent_search WHERE id = ? - 삭제 실행

제안한 방식의 장점

// 제안한 방식
if (!recentSearchJpaRepository.existsById(id)) {
    throw new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND);
}
recentSearchJpaRepository.deleteById(id);

실행되는 쿼리:

  1. SELECT 1 FROM recent_search WHERE id = ? LIMIT 1 - 존재 여부만 확인
  2. DELETE FROM recent_search WHERE id = ? - 삭제 실행

구체적인 효율성 차이

1. 네트워크 트래픽 감소

  • 현재: 전체 엔티티 데이터를 DB → 애플리케이션으로 전송
  • 제안: 존재 여부(boolean)만 전송 → 데이터 전송량 대폭 감소

2. 메모리 사용량 감소

  • 현재: RecentSearchJpaEntity 객체를 메모리에 생성
  • 제안: 불필요한 객체 생성 없음 → 힙 메모리 절약

3. DB 성능 최적화

  • existsById(): DB 옵티마이저가 더 효율적인 실행 계획 수립 가능
  • 인덱스만으로 처리 가능, 실제 데이터 페이지 접근 불필요

실제 운영 환경에서는 이런 작은 최적화들이 누적되어 상당한 성능 차이를 만들어냅니다!


🧠 Learnings used
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.

@RequiredArgsConstructor
public class RecentSearchCreateManager {

private static final String USER_SEARCH_TERM = "사용자 검색";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

하드코딩된 상수를 enum으로 개선 권장

"사용자 검색"이 하드코딩되어 있습니다. SearchType enum을 활용하거나 별도 상수 클래스에서 관리하는 것이 좋겠습니다.

-    private static final String USER_SEARCH_TERM = "사용자 검색";
+    private static final SearchType USER_SEARCH_TYPE = SearchType.USER_SEARCH;

그리고 line 32에서도 수정:

-            RecentSearch userRecentSearch = RecentSearch.withoutId(keyword, USER_SEARCH_TERM, userId);
+            RecentSearch userRecentSearch = RecentSearch.withoutId(keyword, USER_SEARCH_TYPE.getDescription(), userId);
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java
at line 15, the string "사용자 검색" is hardcoded as a constant. Refactor this by
replacing the hardcoded string with a value from the SearchType enum or a
dedicated constants class to improve maintainability and consistency. Also,
apply the same change at line 32 where this string is used.

Comment on lines +20 to +36
public void saveRecentSearchByUser(Long userId, String keyword) {

// 동일 조건 (userId + keyword + type) 검색 기록이 이미 있는지 확인
recentSearchQueryPort.findRecentSearchByKeywordAndUserId(keyword, userId)
.ifPresentOrElse(
existingRecentSearch -> {
// 이미 존재하면 createdAt만 갱신
existingRecentSearch.updateCreatedAt(LocalDateTime.now());
recentSearchCommandPort.update(existingRecentSearch);
},
() -> {
// 없으면 새로 저장
RecentSearch userRecentSearch = RecentSearch.withoutId(keyword, USER_SEARCH_TERM, userId);
recentSearchCommandPort.save(userRecentSearch);
}
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

트랜잭션 관리 추가 고려

메서드에서 조회 후 저장/업데이트 작업을 수행하고 있습니다. 데이터 일관성을 위해 @Transactional 어노테이션 추가를 고려해보세요.

+    @Transactional
     public void saveRecentSearchByUser(Long userId, String keyword) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void saveRecentSearchByUser(Long userId, String keyword) {
// 동일 조건 (userId + keyword + type) 검색 기록이 이미 있는지 확인
recentSearchQueryPort.findRecentSearchByKeywordAndUserId(keyword, userId)
.ifPresentOrElse(
existingRecentSearch -> {
// 이미 존재하면 createdAt만 갱신
existingRecentSearch.updateCreatedAt(LocalDateTime.now());
recentSearchCommandPort.update(existingRecentSearch);
},
() -> {
// 없으면 새로 저장
RecentSearch userRecentSearch = RecentSearch.withoutId(keyword, USER_SEARCH_TERM, userId);
recentSearchCommandPort.save(userRecentSearch);
}
);
}
@Transactional
public void saveRecentSearchByUser(Long userId, String keyword) {
// 동일 조건 (userId + keyword + type) 검색 기록이 이미 있는지 확인
recentSearchQueryPort.findRecentSearchByKeywordAndUserId(keyword, userId)
.ifPresentOrElse(
existingRecentSearch -> {
// 이미 존재하면 createdAt만 갱신
existingRecentSearch.updateCreatedAt(LocalDateTime.now());
recentSearchCommandPort.update(existingRecentSearch);
},
() -> {
// 없으면 새로 저장
RecentSearch userRecentSearch = RecentSearch.withoutId(keyword, USER_SEARCH_TERM, userId);
recentSearchCommandPort.save(userRecentSearch);
}
);
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java
around lines 20 to 36, the method performs read and write operations that should
be atomic to maintain data consistency. Add the @Transactional annotation to the
saveRecentSearchByUser method or the enclosing class to ensure the entire
operation runs within a transaction boundary, preventing partial updates or
inconsistent state.

Comment on lines +29 to +31
public void updateCreatedAt(LocalDateTime localDateTime) {
this.setCreatedAt(localDateTime);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

가변성 설계에 대한 검토가 필요합니다.

updateCreatedAt 메서드가 setCreatedAt을 호출하여 도메인 객체의 상태를 변경하고 있습니다. 이는 BaseDomainEntity가 가변 상태를 허용하도록 수정되었음을 의미합니다. 도메인 객체의 불변성 원칙과 현재 설계 선택에 대한 검토가 필요합니다.

다음 스크립트로 BaseDomainEntity의 변경사항을 확인해 주세요:


🏁 Script executed:

#!/bin/bash
# Description: BaseDomainEntity의 setCreatedAt 메서드 존재 여부와 가변성 설계 확인

# BaseDomainEntity 클래스에서 setCreatedAt 메서드 찾기
ast-grep --pattern $'class BaseDomainEntity {
  $$$
  setCreatedAt($_) {
    $$$
  }
  $$$
}'

# 또는 createdAt 필드와 관련된 setter 메서드들 확인
rg -A 5 -B 5 "setCreatedAt|createdAt.*=" src/main/java/konkuk/thip/common/entity/

Length of output: 950


도메인 객체의 가변성 (createdAt) 재검토 필요
RecentSearch.updateCreatedAt 메서드가 BaseJpaEntity의 프로텍티드 setter를 호출하여 createdAt을 변경하고 있습니다.

  • 확인된 setter 위치
    • 파일: src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java
    • 메서드: protected void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }

도메인 레벨에서 createdAt 변경을 허용할지, 불변성(immutability) 전략을 다시 검토해 주세요.

  • 불변성을 유지하려면 setter 및 updateCreatedAt 제거 또는 별도 빌더/팩토리 패턴, 도메인 이벤트 등 대안 적용을 제안합니다.
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/recentSearch/domain/RecentSearch.java around lines
29 to 31, the updateCreatedAt method modifies the createdAt field via a
protected setter in the base entity, which breaks domain immutability. To fix
this, remove the updateCreatedAt method and avoid exposing any setter for
createdAt. Instead, ensure createdAt is set only once during entity creation,
possibly by using a constructor, builder, or factory method, and consider domain
events if updates are needed without breaking immutability.

Comment on lines +3 to +7
public record UserSearchQuery(
String keyword,
Long userId,
Integer size
) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

필수: 입력 값 검증 로직을 추가해 주세요.

keywordsize 필드에 대한 null/범위 검증이 전혀 없어 서비스 레이어까지 잘못된 값이 전파될 수 있습니다.
레코드의 Canonical Constructor를 오버라이드하여 불변 조건을 선언적으로 보장하거나, 최소한 Objects.requireNonNullrequire(size > 0) 수준의 체크를 권장드립니다.

 public record UserSearchQuery(
         String keyword,
         Long userId,
         Integer size
 ) {
+    public UserSearchQuery {
+        Objects.requireNonNull(keyword, "keyword must not be null");
+        if (keyword.isBlank()) {
+            throw new IllegalArgumentException("keyword must not be blank");
+        }
+        Objects.requireNonNull(size, "size must not be null");
+        if (size <= 0) {
+            throw new IllegalArgumentException("size must be positive");
+        }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public record UserSearchQuery(
String keyword,
Long userId,
Integer size
) {
public record UserSearchQuery(
String keyword,
Long userId,
Integer size
) {
public UserSearchQuery {
Objects.requireNonNull(keyword, "keyword must not be null");
if (keyword.isBlank()) {
throw new IllegalArgumentException("keyword must not be blank");
}
Objects.requireNonNull(size, "size must not be null");
if (size <= 0) {
throw new IllegalArgumentException("size must be positive");
}
}
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/user/application/port/in/dto/UserSearchQuery.java
around lines 3 to 7, the UserSearchQuery record lacks validation for the keyword
and size fields, allowing null or invalid values to propagate. Override the
canonical constructor of the record to add checks that keyword is not null and
size is not null and greater than zero, using Objects.requireNonNull for null
checks and explicit condition checks for size. This will ensure immutability and
enforce input validation at the time of object creation.

Comment on lines +60 to +61
List.of("thipalpha", "thipbeta", "123thip", "thipgamma", "otheruser")
.forEach(nickname -> userJpaRepository.save(TestEntityFactory.createUser(alias, nickname)));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

테스트 데이터의 검색 가중치 검증 필요

PR 목표에 따르면 검색 결과는 가중치에 따라 정렬되어야 합니다 (정확히 일치=3, 시작=2, 포함=1). 현재 테스트 데이터와 예상 결과가 이 가중치 로직을 올바르게 반영하는지 확인이 필요합니다.

다음 스크립트를 실행하여 사용자 검색 서비스의 정렬 로직을 확인해보세요:


🏁 Script executed:

#!/bin/bash
# UserSearchService의 검색 정렬 로직 확인
ast-grep --pattern 'class UserSearchService {
  $$$
}'

Length of output: 56


아래 스크립트를 실행하여 UserSearchService 클래스 위치와 검색 정렬 로직(가중치 적용)을 재확인해 주세요.


🏁 Script executed:

#!/bin/bash
# UserSearchService 클래스 파일 위치 확인
fd UserSearchService.java || true

# 클래스 정의 및 상위 20줄 확인
rg -n "class UserSearchService" -A 20 || true

# 검색 메서드 및 정렬 로직 파악 (메서드명에 search 포함)
rg -n "search" -A 10 -B 5 || true

# 가중치 관련 키워드(search weight) 탐색
rg -n "weight" -A 5 -B 5 || true

Length of output: 167854


아래 스크립트를 실행해 findUsersByNicknameOrderByAccuracy 메서드가 선언된 인터페이스와 그 구현체 위치를 확인하고, 실제 정렬(가중치) 로직이 어디에 정의되어 있는지 파악해 주세요.


🏁 Script executed:

#!/bin/bash
# 1. UserQueryPort 인터페이스 위치 확인
rg -n "interface UserQueryPort" -A 5 -B 5 || true

# 2. findUsersByNicknameOrderByAccuracy 선언/구현 검색
rg -n "findUsersByNicknameOrderByAccuracy" -A 10 -B 5 || true

# 3. UserJpaRepository 에서 유사 메서드 검색 (JPA 메서드 이름 패턴)
rg -n "findByNickname.*OrderByAccuracy" -A 5 -B 5 || true

Length of output: 8347


아직 테스트에서 가중치 기반 정렬 순서(정확히 일치→접두사 일치→포함 일치)가 검증되지 않은 것 같습니다. UserSearchApiTest에 아래와 같이 순서 검증을 추가해 주세요.

  • 파일: src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java
    • 검색 결과 리스트의 각 인덱스별 nickname이 기대값과 일치하는지 검증

예시 추가 코드:

// then
result.andExpect(jsonPath("$.data.userList[0].nickname").value("thip"))        // 정확히 일치
      .andExpect(jsonPath("$.data.userList[1].nickname").value("thipalpha"))   // 접두사 일치
      .andExpect(jsonPath("$.data.userList[2].nickname").value("thipbeta"))    // 접두사 일치
      .andExpect(jsonPath("$.data.userList[3].nickname").value("thipgamma"))   // 접두사 일치
      .andExpect(jsonPath("$.data.userList[4].nickname").value("123thip"));   // 포함 일치

이렇게 하면 findUsersByNicknameOrderByAccuracy 의 CaseBuilder 가중치(3/2/1)가 실제 API 테스트에 반영되었는지 검증할 수 있습니다.

🤖 Prompt for AI Agents
In src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java around
lines 60 to 61, the test does not verify the order of search results based on
weighted sorting (exact match → prefix match → contains match). Add assertions
after the search execution to check that the nicknames in the result list match
the expected order by index, using jsonPath to verify each nickname corresponds
to the correct weighted match category as shown in the example. This ensures the
accuracy-based sorting logic is properly tested in the API.

Copy link
Collaborator

@seongjunnoh seongjunnoh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다! 리뷰 확인 부탁드립니다!

Comment on lines +8 to +14
public record UserQueryDto(Long userId,
String nickname,
String profileImageUrl,
String aliasName,
String aliasColor,
Integer followerCount,
LocalDateTime createdAt) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM
UserQueryDto 라는 네이밍이 훨씬 직관적인 것 같습니다

Comment on lines +55 to +73
return queryFactory
.select(new QUserQueryDto(
user.userId,
user.nickname,
alias.imageUrl,
alias.value,
alias.color,
user.followerCount,
user.createdAt
))
.from(user)
.leftJoin(user.aliasForUserJpaEntity, alias)
.where(user.nickname.like(pattern)
.and(user.userId.ne(userId))
.and(user.status.eq(StatusType.ACTIVE)))
.orderBy(priority.desc(), user.nickname.asc())
.limit(size)
.fetch();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like 명령어를 사용하는 방식으로 다시 수정하셨군요! 이것도 좋은 것 같습니다! 추후에 개선 방법을 같이 논의해봅시다!

Copy link
Contributor Author

@buzz0331 buzz0331 Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

필터링은 크게 like 명령어로 걸지만, 그 안에서 정렬은 가중치를 매기는 방식을 사용했습니다!!
다른 방법은 추후에 인프라를 조정하면서 한번 생각해봐야될 것 같아요,,

Comment on lines +49 to +53
NumberExpression<Integer> priority = new CaseBuilder()
.when(user.nickname.eq(keyword)).then(3)
.when(user.nickname.like(keyword + "%")).then(2)
.when(user.nickname.like(pattern)).then(1)
.otherwise(0);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

.where(user.nickname.like(pattern)
.and(user.userId.ne(userId))
.and(user.status.eq(StatusType.ACTIVE)))
.orderBy(priority.desc(), user.nickname.asc())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment on lines +22 to +24
// 동일 조건 (userId + keyword + type) 검색 기록이 이미 있는지 확인
recentSearchQueryPort.findRecentSearchByKeywordAndUserId(keyword, userId)
.ifPresentOrElse(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3 : 왜 메서드의 인자값으로 type은 넘기지 않고, 메서드 내부에서 jpa repository 를 호출할때 USER_SEARCH 를 주입해주는 건가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 제가 SearchType을 서비스 로직에서 import하여 쓰지 않은 이유와 일맥상통합니다! 다만, 뒷 pr에서 정의한 RecentSearchType을 여기서 활용할 수 있겠네요! 한번 수정해보겠습니다.

Comment on lines +26 to +29
// 이미 존재하면 createdAt만 갱신
existingRecentSearch.updateCreatedAt(LocalDateTime.now());
recentSearchCommandPort.update(existingRecentSearch);
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3 : 엇 createdAt이 udpate 안되지 않나요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네넵 그걸 뒷 pr에서 발견해서 뒤쪽 pr에서 수정하였습니다! 혼란을 드렸네요,, 지송

},
() -> {
// 없으면 새로 저장
RecentSearch userRecentSearch = RecentSearch.withoutId(keyword, USER_SEARCH_TERM, userId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3 : 지금처럼 RecentSearchCreateManager에서 상수로 선언한 String 값을 주입해주는 구조보다는 유지보수를 위해

RecentSearch를 생성할 때 SearchType.USER_SEARCH.getSearchType() 을 주입

하는 방식으로 수정하는건 어떤가요?

현재 User 등 여러 도메인 엔티티에서 위와 같은 방식으로 코드가 작성되어 있긴 합니다!

public class User extends BaseDomainEntity {

    private Long id;

    private String nickname;

    private String userRole;

    private String oauth2Id;

    private Integer followerCount; // 팔로워 수

    private Alias alias;

    public static User withoutId(String nickname, String userRole, String oauth2Id, Alias alias) {
        return User.builder()
                .id(null)
                .nickname(nickname)
                .userRole(userRole)
                .oauth2Id(oauth2Id)
                .followerCount(0)
                .alias(alias)
                .build();
    }
}

public Long signup(UserSignupCommand command) {
        Alias alias = Alias.from(command.aliasName());
        User user = User.withoutId(
                command.nickname(), USER.getType(), command.oauth2Id(), alias
        );

        return userCommandPort.save(user);
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 처음에 이렇게 고려하려고 했습니다! 다만, SearchType은 저희가 adapter.out 계층에서 사용중인 Enum 클래스이기 때문에 해당 클래스를 서비스가 import하고 있는 것은 적절하지 않다고 판단했습니다!

뒷 pr 리뷰하시면 보실 수 있겠지만, Domain 차원에서 param과 SearchType.searchType들을 매핑지어서 활용하도록 했습니다!!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뒷 pr에 관련해서 코멘트 달았습니다!

Comment on lines +38 to +44
@Override
public void delete(Long id) {
recentSearchJpaRepository.delete(
recentSearchJpaRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND))
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 이런 구현 방식도 괜찮은거 같네요

Comment on lines +38 to +44
@Override
public void delete(Long id) {
recentSearchJpaRepository.delete(
recentSearchJpaRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND))
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai 너가 제안한 delete 방식이 현재 구현된 방식보다 어떤 점에서 더 효율적인지 설명해줘

Copy link
Collaborator

@seongjunnoh seongjunnoh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뒷 pr에서 rebase 하신 것 같으니 현재 pr은 머지하셔도 될듯합니다!

논의할 부분은 뒷 pr에서 고고하시져~~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[THIP2025-156] [feat] 사용자 검색 조회 api 개발

2 participants