-
Notifications
You must be signed in to change notification settings - Fork 0
[hotfix] 검색 관련 api 의 request 에 검색중/검색 완료를 구분하는 flag 추가 #208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
18b90dc
146a2a7
c06a484
36aa1c7
05b9a57
e23c805
fea49ff
676aae6
faf2cb3
4976f19
bd95c8c
8250755
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,6 @@ | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| public interface BookSearchUseCase { | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| NaverBookParseResult searchBooks(String keyword, int page,Long userId); | ||||||||||||||||||||||||||||||||||
| BookDetailSearchResult searchDetailBooks(String isbn,Long userId); | ||||||||||||||||||||||||||||||||||
| NaverBookParseResult searchBooks(String keyword, int page, Long userId, boolean isFinalized); | ||||||||||||||||||||||||||||||||||
| BookDetailSearchResult searchDetailBooks(String isbn, Long userId); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+8
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain포트 시그니처 변경에 따른 호출부/구현체 동기화 확인 + 파라미터 의미 Javadoc 추가 제안 boolean isFinalized 추가는 의도와 맞습니다. 다만 외부 공개 포트 변경이므로 모든 구현체와 호출부가 4개 인자 시그니처로 동기화되었는지 확인해 주세요. 또한 isFinalized의 의미(입력 중 vs. 확정)와 저장 정책을 Javadoc으로 명시하면 유지보수성이 좋아집니다. 다음 스크립트로 잠재적 미동기 호출을 점검할 수 있습니다. 🏁 Script executed: #!/bin/bash
# searchBooks 호출부를 전수 조사합니다. (컨텍스트 2줄 포함)
rg -n --no-ignore -S 'searchBooks\(' -A 2 | sed 's/^/>> /'Length of output: 1251
src/main/java/konkuk/thip/book/application/port/in/BookSearchUseCase.java
@@ interface BookSearchUseCase {
- NaverBookParseResult searchBooks(String keyword, int page, Long userId, boolean isFinalized);
+ /**
+ * 네이버 도서 검색을 수행합니다.
+ *
+ * @param keyword 검색어
+ * @param page 페이지 번호 (1부터 시작)
+ * @param userId 사용자 식별자
+ * @param isFinalized 최종 확정 여부.
+ * - false: 사용자가 입력 중인 임시 상태의 결과 조회
+ * - true: 최종 확정된 상태의 결과 조회 및 저장
+ * @return 검색 결과를 담은 {@link NaverBookParseResult}
+ */
+ NaverBookParseResult searchBooks(String keyword, int page, Long userId, boolean isFinalized);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,13 +14,16 @@ public class RecentSearchCreateManager { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final RecentSearchCommandPort recentSearchCommandPort; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final RecentSearchQueryPort recentSearchQueryPort; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void saveRecentSearchByUser(Long userId, String keyword, RecentSearchType type) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void saveRecentSearchByUser(Long userId, String keyword, RecentSearchType type, boolean isFinalized) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 검색완료일 경우에 최근검색어 추가 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isFinalized) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 동일 조건 (userId + keyword + type) 검색 기록이 이미 있는지 확인 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recentSearchQueryPort.findRecentSearchByKeywordAndUserId(keyword, userId, type) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .ifPresentOrElse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recentSearchCommandPort::touch, // 있으면 modifiedAt 갱신 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| () -> recentSearchCommandPort.save(RecentSearch.withoutId(keyword, type, userId)) // 없으면 새로 저장 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+17
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 중복 저장/레이스 대응을 위한 DB 유니크 제약 + 포트 레벨 upsert 권장 현재 find-then-save 패턴은 동시성 하에서 중복 insert 경쟁이 발생할 수 있습니다. (user_id, keyword, type) 유니크 인덱스를 추가하고, 포트에 upsert(or touch) API를 도입해 단일 쿼리로 처리하면 안전성과 성능이 개선됩니다. 예시:
🛠️ Refactor suggestion isFinalized=true라도 userId null/keyword 공백 저장을 차단해야 합니다 익명 요청(userId=null) 또는 공백 키워드가 저장 경로로 들어오면 제약 조건 위반/데이터 오염 가능성이 있습니다. 매니저 레벨에서 가드하면 모든 호출부(Book/Room/User)에서 일관되게 방어됩니다. 적용 제안(diff): - public void saveRecentSearchByUser(Long userId, String keyword, RecentSearchType type, boolean isFinalized) {
- // 검색완료일 경우에 최근검색어 추가
- if (isFinalized) {
- // 동일 조건 (userId + keyword + type) 검색 기록이 이미 있는지 확인
- recentSearchQueryPort.findRecentSearchByKeywordAndUserId(keyword, userId, type)
- .ifPresentOrElse(
- recentSearchCommandPort::touch, // 있으면 modifiedAt 갱신
- () -> recentSearchCommandPort.save(RecentSearch.withoutId(keyword, type, userId)) // 없으면 새로 저장
- );
- }
- }
+ public void saveRecentSearchByUser(Long userId, String keyword, RecentSearchType type, boolean isFinalized) {
+ // 입력 중이면 write 방지
+ if (!isFinalized) {
+ return;
+ }
+ // 익명/비정상 요청 방어
+ if (userId == null) {
+ return;
+ }
+ // 공백/빈 키워드 저장 방지 + 정규화
+ final String normalizedKeyword = keyword == null ? "" : keyword.trim();
+ if (normalizedKeyword.isEmpty()) {
+ return;
+ }
+ // 동일 조건 (userId + keyword + type) 존재 시 touch, 없으면 저장
+ recentSearchQueryPort.findRecentSearchByKeywordAndUserId(normalizedKeyword, userId, type)
+ .ifPresentOrElse(
+ recentSearchCommandPort::touch,
+ () -> recentSearchCommandPort.save(RecentSearch.withoutId(normalizedKeyword, type, userId))
+ );
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 동일 조건 (userId + keyword + type) 검색 기록이 이미 있는지 확인 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recentSearchQueryPort.findRecentSearchByKeywordAndUserId(keyword, userId, type) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .ifPresentOrElse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recentSearchCommandPort::touch, // 있으면 modifiedAt 갱신 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| () -> recentSearchCommandPort.save(RecentSearch.withoutId(keyword, type, userId)) // 없으면 새로 저장 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,13 @@ | ||
| package konkuk.thip.room.application.service; | ||
|
|
||
| import konkuk.thip.common.exception.BusinessException; | ||
| import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; | ||
| import konkuk.thip.recentSearch.application.service.manager.RecentSearchCreateManager; | ||
| import konkuk.thip.room.adapter.in.web.response.RoomSearchResponse; | ||
| import konkuk.thip.room.domain.Category; | ||
| import konkuk.thip.room.adapter.out.persistence.RoomSearchSortParam; | ||
| import konkuk.thip.room.application.port.in.RoomSearchUseCase; | ||
| import konkuk.thip.room.application.port.out.RoomQueryPort; | ||
| import konkuk.thip.room.domain.Category; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.PageRequest; | ||
|
|
@@ -14,7 +16,6 @@ | |
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import static konkuk.thip.common.exception.code.ErrorCode.CATEGORY_NOT_FOUND; | ||
| import static konkuk.thip.common.exception.code.ErrorCode.INVALID_ROOM_SEARCH_SORT; | ||
|
|
||
| @Service | ||
|
|
@@ -25,9 +26,11 @@ public class RoomSearchService implements RoomSearchUseCase { | |
|
|
||
| private final RoomQueryPort roomQueryPort; | ||
|
|
||
| private final RecentSearchCreateManager recentSearchCreateManager; | ||
|
|
||
| @Override | ||
| @Transactional(readOnly = true) | ||
| public RoomSearchResponse searchRoom(String keyword, String category, String sort, int page) { | ||
| @Transactional // <- 최근 검색 저장으로 인한 트랜잭션 | ||
| public RoomSearchResponse searchRoom(String keyword, String category, String sort, int page, boolean isFinalized, Long userId) { | ||
| // 1. validation | ||
| String sortVal = validateSort(sort); | ||
| String categoryVal = validateCategory(category); | ||
|
|
@@ -39,6 +42,9 @@ public RoomSearchResponse searchRoom(String keyword, String category, String sor | |
| // 3. 방 검색 | ||
| Page<RoomSearchResponse.RoomSearchResult> result = roomQueryPort.searchRoom(keyword, categoryVal, pageable); | ||
|
|
||
| // TODO 검색 완료일 경우, 최근 검색어로 저장되도록 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 파라미터 미사용(TODO만 존재) — 모듈 간 동작 일관성 유지를 위해 즉시 구현 권장 Book/User 모듈과 동일하게 isFinalized==true일 때만 최근검색어를 저장하도록 구현하면 UX/데이터 일관성이 확보됩니다. 성능 측면에서도 입력 중 요청의 쓰기를 배제해 DB 부하와 write amplification을 줄일 수 있습니다. 원하시면 RecentSearchCommandPort 주입 및 저장 블록(중복/널 가드 포함)까지 패치 제안 드리겠습니다. 🤖 Prompt for AI Agents |
||
| recentSearchCreateManager.saveRecentSearchByUser(userId, keyword, RecentSearchType.ROOM_SEARCH, isFinalized); | ||
|
|
||
| // 4. response 구성 | ||
| return new RoomSearchResponse( | ||
| result.getContent(), | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -120,9 +120,10 @@ public BaseResponse<UserIsFollowingResponse> checkIsFollowing( | |||||||
| @GetMapping("/users") | ||||||||
| public BaseResponse<UserSearchResponse> showSearchUsers( | ||||||||
| @Parameter(description = "검색어", example = "thip") @RequestParam @NotBlank(message = "검색어는 필수입니다.") final String keyword, | ||||||||
| @Parameter(description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", example = "false") @RequestParam(name = "isFinalized") final boolean isFinalized, | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 필수 파라미터 추가로 인한 API 호환성 깨짐 — 기본값 false로 다운타임 없이 적용 권장 현재 isFinalized가 필수(boolean, defaultValue 미지정)여서 기존 클라이언트는 즉시 400 Bad Request를 받을 가능성이 큽니다. hotfix 성격이라면 점진적 적용을 위해 기본값을 false로 두고 optional로 받는 것이 안전합니다. 적용 diff: -@Parameter(description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", example = "false") @RequestParam(name = "isFinalized") final boolean isFinalized,
+@Parameter(description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", example = "false")
+@RequestParam(name = "isFinalized", required = false, defaultValue = "false") final boolean isFinalized,📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||
| @Parameter(description = "단일 검색 결과 페이지 크기 (1~30) / default : 30", example = "30") @RequestParam(required = false, defaultValue = "30") @Min(1) @Max(30) final Integer size, | ||||||||
| @Parameter(hidden = true) @UserId final Long userId) { | ||||||||
| return BaseResponse.ok(userSearchUsecase.searchUsers(UserSearchQuery.of(keyword, userId, size))); | ||||||||
| return BaseResponse.ok(userSearchUsecase.searchUsers(UserSearchQuery.of(keyword, userId, size, isFinalized))); | ||||||||
| } | ||||||||
|
|
||||||||
| @Operation( | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,9 +29,8 @@ public UserSearchResponse searchUsers(UserSearchQuery userSearchQuery) { | |
| userSearchQuery.size() | ||
| )); | ||
|
|
||
| // 최근 검색어 저장 | ||
| recentSearchCreateManager.saveRecentSearchByUser(userSearchQuery.userId(), userSearchQuery.keyword(), RecentSearchType.USER_SEARCH); | ||
|
|
||
| recentSearchCreateManager.saveRecentSearchByUser(userSearchQuery.userId(), userSearchQuery.keyword(), RecentSearchType.USER_SEARCH, userSearchQuery.isFinalized()); | ||
|
|
||
|
Comment on lines
+32
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain테스트: isFinalized=true/false 분기 동작 검증 추가 권장 isFinalized=true일 때만 저장이 발생하고 false일 때는 호출되지 않음을 검증하는 단위/슬라이스 테스트가 필요합니다(Mock 기반 verify). 다음 스크립트로 테스트 존재 여부를 빠르게 탐색할 수 있습니다: 🏁 Script executed: #!/bin/bash
# isFinalized 분기 검증 및 4-인자 시그니처 전환 적용 여부 점검
echo "1) 최근검색 저장 호출 위치 검색"
rg -n "saveRecentSearchByUser\\(" -A 3
echo ""
echo "2) isFinalized 사용 테스트 존재 여부"
fd -t f -e java -e kt test | xargs rg -n "isFinalized\\(|isFinalized\\s*=" -A 2
echo ""
echo "3) UserSearchService 테스트에서 저장 호출 verify 여부"
rg -n "UserSearchService" -A 5 test | rg -n "RecentSearchCreateManager|saveRecentSearchByUser|verify\\("Length of output: 2624 isFinalized 분기 동작 검증용 단위 테스트 추가 필요
🤖 Prompt for AI Agents💡 Verification agent 🧩 Analysis chain최근검색 저장 실패가 전체 검색을 실패시키지 않도록 격리 필요 현재 권장 대응:
간단한 try-catch 적용 예시는 다음과 같습니다: - recentSearchCreateManager.saveRecentSearchByUser(userSearchQuery.userId(), userSearchQuery.keyword(), RecentSearchType.USER_SEARCH, userSearchQuery.isFinalized());
+ try {
+ recentSearchCreateManager.saveRecentSearchByUser(
+ userSearchQuery.userId(),
+ userSearchQuery.keyword(),
+ RecentSearchType.USER_SEARCH,
+ userSearchQuery.isFinalized()
+ );
+ } catch (Exception e) {
+ log.warn("최근검색 저장 실패 - userId={}, keyword={}", userSearchQuery.userId(), userSearchQuery.keyword(), e);
+ }위 변경을 위해 로거 추가가 필요합니다(아래 코드 추가): // 클래스 상단
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class UserSearchService implements UserSearchUsecase {
...
}검증 요청:
RecentSearchCreateManager와 saveRecentSearchByUser 구현을 확인하기 위해 아래 스크립트를 실행하겠습니다. 🏁 Script executed: #!/bin/bash
# RecentSearchCreateManager 클래스 정의 위치 확인
rg "class RecentSearchCreateManager" -n src
# saveRecentSearchByUser 메서드 구현 위치 확인
rg "saveRecentSearchByUser" -n srcLength of output: 1059 아래 스크립트로 🏁 Script executed: #!/bin/bash
# 클래스 선언부 및 saveRecentSearchByUser 메서드 내용 출력
sed -n '1,200p' src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.javaLength of output: 1365 아래 스크립트를 실행해 🏁 Script executed: #!/bin/bash
sed -n '1,200p' src/main/java/konkuk/thip/user/application/service/UserSearchService.javaLength of output: 1689 최근검색 저장 실패 시 전체 검색 트랜잭션 격리 및 예외 흡수 필요 현재 수정안:
위 변경으로 검색 결과는 항상 반환되며, 최근검색 저장 실패 시에도 전체 트랜잭션이 롤백되지 않습니다. 🤖 Prompt for AI Agents |
||
| return UserSearchResponse.of(userDtoList); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API 호환성 이슈: isFinalized를 선택값 + 기본값 false로 처리해 기존 클라이언트 파손 방지
현재 boolean 원시 타입으로 @RequestParam을 추가하여 필수 파라미터가 되었습니다. 기존 호출에서 해당 쿼리 파라미터를 누락하면 400이 발생합니다. hotfix 상황에서는 하위 호환성을 위해 defaultValue="false"로 두고 required=false를 권장합니다.
권장 변경(diff):
📝 Committable suggestion
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 파라미터는 필수 파라미터를 의도했으니 required = false 는 적절하지 않다고 생각해.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.