[FEAT] 검색 기능 구현#16
Conversation
Walkthrough파트 검색 기능(검색 API/DTO, 리포지토리, 유스케이스, SearchViewModel, 검색 UI/검색 결과 뷰 및 PartView DI 연결)이 추가되었고 TabView 및 일부 뷰에 배경 스타일이 적용되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User as 사용자
participant PartView as PartView
participant SearchVM as SearchViewModel
participant SearchUC as SearchPartsUseCase
participant API as PartAPI
participant ResultView as SearchResultView
User->>PartView: 검색어 입력
PartView->>SearchVM: .search(keyword)
Note right of SearchVM #dfefff: 디바운싱(300ms)\n이전 작업 취소
SearchVM->>SearchUC: execute(keyword, page: 0)
SearchUC->>API: searchParts(keyword, page, size)
API-->>SearchUC: (results, hasMore)
SearchUC-->>SearchVM: (results, hasMore)
SearchVM->>PartView: uiState 업데이트
PartView->>ResultView: 검색 결과 표시
User->>ResultView: 스크롤(끝 근처)
ResultView->>SearchVM: loadMore
SearchVM->>SearchUC: execute(keyword, page: n+1)
SearchUC->>API: searchParts(...)
API-->>SearchUC: (results, hasMore)
SearchUC-->>SearchVM: (results, hasMore)
SearchVM->>ResultView: 추가 결과 제공
User->>ResultView: 항목 선택
ResultView->>SearchVM: showBottomSheet(part)
SearchVM->>ResultView: selectedPart 설정 -> 바텀시트 표시
Estimated code review effort🎯 3 (중간) | ⏱️ ~25분 Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (3)
💤 Files with no reviewable changes (1)
🧰 Additional context used🧬 Code graph analysis (1)SampoomManagement/App/ContentView.swift (1)
🔇 Additional comments (4)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
SampoomManagement/Features/Part/Domain/Repository/PartRepository.swift (1)
14-14: 프로토콜 확장이 적절하게 추가됨검색 메서드가 Repository 프로토콜에 올바르게 추가되었습니다. 튜플 반환 방식은 간단한 페이지네이션에 적합합니다.
향후 페이지네이션 정보가 더 복잡해질 경우, 전용
PaginatedResponse<T>타입을 고려해볼 수 있습니다:struct PaginatedResponse<T> { let results: [T] let hasMore: Bool let currentPage: Int let totalPages: Int }하지만 현재 구현으로도 충분합니다.
SampoomManagement/Features/Part/UI/SearchViewModel.swift (2)
70-93: 불필요한MainActor.run호출이 있습니다.클래스가 이미
@MainActor로 표시되어 있으므로, 메서드 내부에서 명시적으로await MainActor.run을 호출할 필요가 없습니다. 모든 메서드가 이미 메인 액터에서 실행됩니다.다음과 같이 리팩토링하세요:
do { let (results, hasMore) = try await searchPartsUseCase.execute(keyword: keyword, page: 0) - await MainActor.run { - uiState = uiState.copy( - searchResults: results, - isSearching: false, - searchError: nil, - currentPage: 0, - hasMorePages: hasMore - ) - } + uiState = uiState.copy( + searchResults: results, + isSearching: false, + searchError: nil, + currentPage: 0, + hasMorePages: hasMore + ) } catch { - await MainActor.run { - uiState = uiState.copy( - searchResults: [], - isSearching: false, - searchError: error.localizedDescription, - currentPage: 0, - hasMorePages: false - ) - } + uiState = uiState.copy( + searchResults: [], + isSearching: false, + searchError: error.localizedDescription, + currentPage: 0, + hasMorePages: false + ) }
103-125: 불필요한MainActor.run호출이 있습니다.
loadMoreResults메서드에도 동일하게 불필요한await MainActor.run블록이 있습니다.다음과 같이 제거하세요:
Task { do { let nextPage = uiState.currentPage + 1 let (newResults, hasMore) = try await searchPartsUseCase.execute(keyword: currentKeyword, page: nextPage) - await MainActor.run { - let combinedResults = uiState.searchResults + newResults - uiState = uiState.copy( - searchResults: combinedResults, - currentPage: nextPage, - hasMorePages: hasMore, - isLoadingMore: false - ) - } + let combinedResults = uiState.searchResults + newResults + uiState = uiState.copy( + searchResults: combinedResults, + currentPage: nextPage, + hasMorePages: hasMore, + isLoadingMore: false + ) } catch { - await MainActor.run { - uiState = uiState.copy( - searchError: error.localizedDescription, - isLoadingMore: false - ) - } + uiState = uiState.copy( + searchError: error.localizedDescription, + isLoadingMore: false + ) } }SampoomManagement/Features/Part/UI/SearchResultView.swift (1)
27-44: 중복된 초기화 로직이 있습니다.시트의
onAppear에서partDetailViewModel.onEvent(.initialize(selectedPart))를 호출하고 있지만,SearchViewModel.showBottomSheet(Line 142)에서 이미 동일한 초기화를 수행합니다. 이는 불필요한 중복 호출입니다.시트의
onAppear핸들러를 제거하세요:.sheet(isPresented: $showBottomSheet) { if let selectedPart = viewModel.uiState.selectedPart { PartDetailBottomSheetView(viewModel: partDetailViewModel) - .onAppear { - partDetailViewModel.onEvent(.initialize(selectedPart)) - } .onDisappear { showBottomSheet = false viewModel.onEvent(.dismissBottomSheet) } .presentationDetents([.fraction(0.3)]) .presentationDragIndicator(.visible) .presentationBackground(.clear) } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
SampoomManagement/App/ContentView.swift(2 hunks)SampoomManagement/Core/DI/AppDependencies.swift(3 hunks)SampoomManagement/Core/Network/NetworkManager.swift(1 hunks)SampoomManagement/Core/Resources/StringResources.swift(1 hunks)SampoomManagement/Features/Part/Data/Mappers/PartMappers.swift(1 hunks)SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift(2 hunks)SampoomManagement/Features/Part/Data/Remote/DTO/SearchDataDTO.swift(1 hunks)SampoomManagement/Features/Part/Data/Repository/PartRepositoryImpl.swift(1 hunks)SampoomManagement/Features/Part/Domain/Models/SearchResult.swift(1 hunks)SampoomManagement/Features/Part/Domain/Repository/PartRepository.swift(1 hunks)SampoomManagement/Features/Part/Domain/UseCase/SearchPartsUseCase.swift(1 hunks)SampoomManagement/Features/Part/UI/PartView.swift(1 hunks)SampoomManagement/Features/Part/UI/SearchResultView.swift(1 hunks)SampoomManagement/Features/Part/UI/SearchUiEvent.swift(1 hunks)SampoomManagement/Features/Part/UI/SearchUiState.swift(1 hunks)SampoomManagement/Features/Part/UI/SearchViewModel.swift(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (9)
SampoomManagement/Features/Part/UI/SearchUiEvent.swift (1)
SampoomManagement/Features/Part/UI/SearchViewModel.swift (3)
clearSearch(128-138)showBottomSheet(140-143)dismissBottomSheet(145-148)
SampoomManagement/Features/Part/Domain/UseCase/SearchPartsUseCase.swift (3)
SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift (1)
searchParts(45-62)SampoomManagement/Features/Part/Data/Repository/PartRepositoryImpl.swift (1)
searchParts(29-31)SampoomManagement/Features/Part/UI/SearchViewModel.swift (1)
searchParts(41-94)
SampoomManagement/Features/Part/UI/PartView.swift (1)
SampoomManagement/Features/Part/UI/SearchViewModel.swift (2)
onEvent(26-39)clearSearch(128-138)
SampoomManagement/Features/Part/UI/SearchResultView.swift (1)
SampoomManagement/Features/Part/UI/SearchViewModel.swift (3)
showBottomSheet(140-143)onEvent(26-39)dismissBottomSheet(145-148)
SampoomManagement/Features/Part/Domain/Repository/PartRepository.swift (3)
SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift (1)
searchParts(45-62)SampoomManagement/Features/Part/Data/Repository/PartRepositoryImpl.swift (1)
searchParts(29-31)SampoomManagement/Features/Part/UI/SearchViewModel.swift (1)
searchParts(41-94)
SampoomManagement/Features/Part/Data/Repository/PartRepositoryImpl.swift (2)
SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift (1)
searchParts(45-62)SampoomManagement/Features/Part/UI/SearchViewModel.swift (1)
searchParts(41-94)
SampoomManagement/Features/Part/UI/SearchViewModel.swift (4)
SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift (1)
searchParts(45-62)SampoomManagement/Features/Part/Data/Repository/PartRepositoryImpl.swift (1)
searchParts(29-31)SampoomManagement/Features/Part/UI/SearchUiState.swift (1)
copy(37-55)SampoomManagement/Features/Part/Domain/UseCase/SearchPartsUseCase.swift (1)
execute(17-19)
SampoomManagement/App/ContentView.swift (1)
SampoomManagement/Core/DI/AppDependencies.swift (1)
makeSearchViewModel(137-140)
SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift (3)
SampoomManagement/Features/Part/Data/Repository/PartRepositoryImpl.swift (1)
searchParts(29-31)SampoomManagement/Core/Network/NetworkManager.swift (1)
request(18-56)SampoomManagement/Features/Part/Data/Mappers/PartMappers.swift (5)
toModel(11-17)toModel(21-28)toModel(32-39)toModel(43-53)toModel(57-61)
🔇 Additional comments (10)
SampoomManagement/Core/Network/NetworkManager.swift (1)
32-32: GET과 다른 HTTP 메서드에 대한 적절한 인코딩 방식 적용GET 요청은 URLEncoding으로 쿼리 파라미터를, 다른 메서드는 JSONEncoding으로 body를 인코딩하는 것이 REST API의 표준 관행입니다. 구현이 정확합니다.
SampoomManagement/Features/Part/Data/Repository/PartRepositoryImpl.swift (1)
29-31: 깔끔한 Repository 구현API 레이어로의 단순 위임 패턴이 적절하게 적용되었습니다. Repository 레이어에서 추가적인 비즈니스 로직이 필요 없는 경우 이러한 구조가 적합합니다.
SampoomManagement/Features/Part/UI/SearchUiEvent.swift (1)
10-16: 명확한 이벤트 기반 아키텍처검색 플로우에 필요한 이벤트들이 잘 정의되어 있습니다. 이벤트 기반 설계는 UI 상태 관리와 비즈니스 로직의 분리를 명확하게 합니다.
SampoomManagement/Features/Part/Data/Mappers/PartMappers.swift (2)
42-54: 중첩된 DTO를 평탄화하는 매핑 로직
flatMap을 사용하여 category → groups → parts 구조를[SearchResult]로 올바르게 변환하고 있습니다. 각 결과에 카테고리명과 그룹명을 주입하는 방식도 적절합니다.
56-62: SearchDataDTO의 간결한 변환 구현content 배열을 flatMap으로 처리하여 모든 검색 결과를 단일 배열로 평탄화하는 방식이 명확하고 효율적입니다.
SampoomManagement/Core/Resources/StringResources.swift (2)
72-73: 공통 문자열 리소스 추가에러 처리 및 재시도 관련 문자열이
Common구조체에 적절하게 추가되었습니다.
76-82: 검색 기능 전용 문자열 리소스 구성검색 UI에 필요한 문자열들이 전용 네임스페이스로 잘 구성되어 있습니다. 명확한 네이밍과 한글 메시지가 적절합니다.
SampoomManagement/Features/Part/Domain/Models/SearchResult.swift (1)
10-14: 명확하게 정의된 검색 결과 모델검색 결과를 표현하는 도메인 모델이 적절하게 정의되었습니다.
Equatable준수는 리스트 비교 및 UI 업데이트 최적화에 유용합니다.SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift (2)
9-9: 필요한 import 추가
HTTPMethod타입을 사용하기 위해 Alamofire import가 추가되었습니다.
45-62: 검색 API 구현이 전반적으로 양호함GET 메서드 사용, 페이지네이션 로직(
hasMore계산), 그리고 방어적 nil 처리가 적절합니다.Line 47의 엔드포인트에 하드코딩된
agency/1에 주목해주세요. 현재 구조가 단일 대리점 전용이라면 문제없지만, 향후 멀티 테넌시를 지원해야 한다면 agency ID를 동적으로 주입하는 방식으로 리팩토링이 필요할 수 있습니다. 애플리케이션 요구사항을 확인해주세요.
📝 Summary
TabBar Searchable + PagingContainer 사용하여 검색 기능을 구현했습니다
🙏 Question & PR point
📬 Reference
Summary by CodeRabbit
새로운 기능
스타일 / UI 변경
문구