Skip to content

[FEAT] 검색 기능 구현#16

Merged
Sangyoon98 merged 3 commits intodevfrom
SPM-223
Oct 24, 2025
Merged

[FEAT] 검색 기능 구현#16
Sangyoon98 merged 3 commits intodevfrom
SPM-223

Conversation

@Sangyoon98
Copy link
Copy Markdown
Member

@Sangyoon98 Sangyoon98 commented Oct 23, 2025

📝 Summary

TabBar Searchable + PagingContainer 사용하여 검색 기능을 구현했습니다

🙏 Question & PR point

📬 Reference

Summary by CodeRabbit

  • 새로운 기능

    • 부품 검색 기능 추가(검색어 입력, 결과 목록, 부품 상세 하단 시트)
    • 검색 결과 무한 스크롤 및 페이지네이션, 검색 흐름(취소·디바운스) 지원
    • 검색용 API/모델 및 뷰모델/유스케이스 연계 추가
  • 스타일 / UI 변경

    • 탭 뷰에 시스템 자동 스타일 적용 및 전체 배경 색상 적용
    • 검색 프롬프트 텍스트 변경: "부품 검색"
  • 문구

    • 공통 오류/재시도 문구 및 검색 관련 문자열 리소스 추가

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Oct 23, 2025

Walkthrough

파트 검색 기능(검색 API/DTO, 리포지토리, 유스케이스, SearchViewModel, 검색 UI/검색 결과 뷰 및 PartView DI 연결)이 추가되었고 TabView 및 일부 뷰에 배경 스타일이 적용되었습니다.

Changes

Cohort / File(s) 변경 요약
앱 진입부 · DI
SampoomManagement/App/ContentView.swift, SampoomManagement/Core/DI/AppDependencies.swift
PartViewsearchViewModel 주입 추가; AppDependenciessearchPartsUseCase 프로퍼티 및 makeSearchViewModel() 팩토리 추가; TabView 스타일·배경 변경.
네트워크 레이어
SampoomManagement/Core/Network/NetworkManager.swift
요청 인코딩 분기 추가(GET → URLEncoding.default, 그 외 → JSONEncoding.default).
원격 API · DTO
SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift, SampoomManagement/Features/Part/Data/Remote/DTO/SearchDataDTO.swift
PartAPI.searchParts(keyword:page:size) 비동기 메서드 추가 및 검색 응답용 DTO(SearchDataDTO, SearchCategoryDTO, SearchGroupDTO) 정의.
데이터 매퍼
SampoomManagement/Features/Part/Data/Mappers/PartMappers.swift
SearchCategoryDTO/SearchDataDTO -> [SearchResult] 변환 로직 추가(중첩 평탄화 및 category/group 주입).
리포지토리 · 유스케이스
SampoomManagement/Features/Part/Data/Repository/PartRepositoryImpl.swift, SampoomManagement/Features/Part/Domain/Repository/PartRepository.swift, SampoomManagement/Features/Part/Domain/UseCase/SearchPartsUseCase.swift
리포지토리 프로토콜/구현에 searchParts(keyword:page:) 추가, SearchPartsUseCase 추가(리포지토리 호출 위임).
도메인 모델
SampoomManagement/Features/Part/Domain/Models/SearchResult.swift
SearchResult 구조체 추가(part, categoryName, groupName).
문자열 리소스
SampoomManagement/Core/Resources/StringResources.swift
Commonerror, retry 상수 추가 및 SearchParts 문자열 그룹 추가(title, placeholder, emptyMessage, loadingMessage).
검색 UI · 상태 · 이벤트 · VM
SampoomManagement/Features/Part/UI/SearchUiEvent.swift, SampoomManagement/Features/Part/UI/SearchUiState.swift, SampoomManagement/Features/Part/UI/SearchViewModel.swift, SampoomManagement/Features/Part/UI/SearchResultView.swift, SampoomManagement/Features/Part/UI/PartView.swift
검색 UI 이벤트 열거형 및 불변 UI 상태 구조체 추가, SearchViewModel 추가(디바운싱·취소·페이징·바텀시트 연동), SearchResultView 추가(로딩/오류/빈/결과·무한스크롤), PartViewsearchViewModel 관찰 및 검색 오버레이 통합.
사소한 UI 변경
SampoomManagement/App/RootView.swift, SampoomManagement/Features/Part/UI/PartListView.swift
루트 뷰 배경 적용 및 로딩 상태의 배경 modifier 삭제(시각적 변경).

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 설정 -> 바텀시트 표시
Loading

Estimated code review effort

🎯 3 (중간) | ⏱️ ~25분

Possibly related PRs

Suggested labels

ready-to-merge

Suggested reviewers

  • CHOOSLA
  • Lee-Jong-Jin
  • vivivim
  • taemin3

Poem

"나는 토끼, 부품 찾기 달인,
검색창에 껑충 뛰어들어 디바운스 춤추네.
페이지를 넘기며 결과를 모아, 바텀시트에 빛을 비추네.
작은 발자국 남기면 답이 와요—퐁당! 🐰✨"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed PR 제목 "[FEAT] 검색 기능 구현"은 변경사항과 완전히 관련되어 있습니다. 전체 PR은 TabBar의 Searchable과 PagingContainer를 사용하여 검색 기능을 구현하는 것으로, SearchViewModel, SearchResultView, SearchPartsUseCase 등 검색 기능을 위한 새로운 컴포넌트들이 추가되고 있습니다. 제목은 명확하고 간결하며, [FEAT] 태그로 새로운 기능임을 명시하고 주요 변경사항을 요약하고 있습니다. 파일 목록이나 모호한 용어 없이 개발자의 관점에서 주요 기능 추가를 명확하게 전달합니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch SPM-223

📜 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 150c1f9 and 75d346f.

📒 Files selected for processing (3)
  • SampoomManagement/App/ContentView.swift (3 hunks)
  • SampoomManagement/App/RootView.swift (1 hunks)
  • SampoomManagement/Features/Part/UI/PartListView.swift (0 hunks)
💤 Files with no reviewable changes (1)
  • SampoomManagement/Features/Part/UI/PartListView.swift
🧰 Additional context used
🧬 Code graph analysis (1)
SampoomManagement/App/ContentView.swift (1)
SampoomManagement/Core/DI/AppDependencies.swift (1)
  • makeSearchViewModel (137-140)
🔇 Additional comments (4)
SampoomManagement/App/RootView.swift (1)

67-67: 배경 색상 적용이 적절합니다.

루트 뷰 전체에 일관된 배경 색상을 적용하는 것은 좋은 접근입니다. 다만, ContentView와 중복으로 .background(Color.background)가 적용되는데, 이것이 의도된 것인지 확인해 주세요.

SampoomManagement/App/ContentView.swift (3)

44-44: 대시보드 배경 스타일 적용 확인.

대시보드 뷰에 배경 색상을 적용한 것은 적절합니다.


149-150: TabView 스타일 및 배경 설정 확인.

.tabViewStyle(.automatic)과 배경 색상 적용이 추가되었습니다. .automatic은 시스템 기본 스타일이므로 적절하며, 일관된 배경 색상 적용도 좋습니다.

다만, 여러 레벨(RootView의 Group, ContentView의 TabView, 개별 탭의 NavigationStack)에 중복으로 배경이 적용되는데, 이것이 의도한 것인지 확인하고 불필요한 중복이 있다면 제거하는 것을 고려해 주세요.


123-124: PartView 초기화의 searchViewModel DI 연결이 올바르게 구현되었습니다.

검증 결과:

  • PartView 이니셜라이저 서명(PartView.swift 17-21줄): onNavigatePartList, viewModel, searchViewModel 순서로 정의
  • ContentView.swift 119-125줄의 호출: 모든 파라미터가 정확한 순서로 제공됨
    • Line 123: viewModel: partViewModel
    • Line 124: searchViewModel: dependencies.makeSearchViewModel()
  • 파라미터 타입 및 순서 일치 확인 완료

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 85eb93f and 538b3f0.

📒 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를 동적으로 주입하는 방식으로 리팩토링이 필요할 수 있습니다. 애플리케이션 요구사항을 확인해주세요.

Comment thread SampoomManagement/Features/Part/UI/SearchUiState.swift
Copy link
Copy Markdown
Member

@CHOOSLA CHOOSLA left a comment

Choose a reason for hiding this comment

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

확인했습니다!

Copy link
Copy Markdown

@yangjiseonn yangjiseonn left a comment

Choose a reason for hiding this comment

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

확인했습니다!

Copy link
Copy Markdown

@Lee-Jong-Jin Lee-Jong-Jin left a comment

Choose a reason for hiding this comment

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

iOS도 검색기능이

@33Auto-Bot 33Auto-Bot added the ready-to-merge 3명 이상의 리뷰어에게 승인되어 병합 준비가 완료된 PR label Oct 24, 2025
@Sangyoon98 Sangyoon98 merged commit ebe57ca into dev Oct 24, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge 3명 이상의 리뷰어에게 승인되어 병합 준비가 완료된 PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants