Conversation
…were not tappable
Walkthrough라이브러리(BookInfo) 검색 결과를 추가하고 셀 탭 시 도서 상세로 이동하는 흐름을 도입했다. Diffable DataSource와 상태/액션을 확장했으며, 코디네이터에 상세 화면 전환 메서드를 추가했다. 검색 셀 스타일 파라미터를 확장하고, 요약 뷰의 레이아웃 우선순위와 저자 플레이스홀더를 조정했다. 일부 로깅을 제거했다. Changes
Sequence Diagram(s)sequenceDiagram
participant U as User
participant V as SearchView
participant VC as SearchViewController
participant C as SearchCoordinator
participant B as BookDetailCoordinator
U->>V: 탭 on libraryResult(BookInfo)
V-->>VC: event .goToBookDetail(isbn, userBookId)
Note over VC: throttle(800ms)
VC->>C: didTapBookDetail(isbn, userBookId)
C->>B: init + start(isbn, userBookId)
B-->>C: pushes BookDetail
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (7)
src/Projects/BKDesign/Sources/Components/Summary/BKBookSummaryView.swift (2)
195-201: 저자/출판사 라벨 우선순위 조정 의도는 타당 — separator의 Required 우선순위가 과도할 수 있음authorLabel에 높은 Hugging/Compression을 주고 publisherLabel의 Compression을 낮춘 구조는 “저자 우선 노출” 의도에 부합합니다. 다만 현재 separatorLabel은 Hugging/Compression 모두 .required로 고정되어 있어, 긴 저자/출판사 조합에서 불필요한 잘림을 유발할 수 있습니다(한 글자 폭임에도 공간 경쟁에서 항상 이김).
- 권장: separatorLabel의 Hugging만 .defaultLow로 낮추고 Compression은 그대로 .required 유지. 이러면 폭 부족 시 저자/출판사 텍스트가 먼저 줄어들고, 구분자 유지도 가능합니다.
- separatorLabel.setContentHuggingPriority(.required, for: .horizontal) + separatorLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) separatorLabel.setContentCompressionResistancePriority(.required, for: .horizontal)또한 Dynamic Type(큰 글자 크기), RTL, 긴 한글/영문 혼합 케이스로 실제 트렁케이션 동작을 한 번만 더 점검해 주세요.
248-251: 공백 전처리 및 구분자 노출 조건 개선 제안빈 문자열만 처리하면 공백-only(" ")은 놓칩니다. 또한 저자 또는 출판사가 비어있을 때는 구분자 “ | ”를 숨기는 편이 자연스럽습니다. 아래처럼 미세 개선을 고려해 주세요.
- let wrappedAuthor = author.isEmpty ? "저자 정보 없음" : author + let normalizedAuthor = author.trimmingCharacters(in: .whitespacesAndNewlines) + let normalizedPublisher = publisher.trimmingCharacters(in: .whitespacesAndNewlines) + let wrappedAuthor = normalizedAuthor.isEmpty ? "저자 정보 없음" : normalizedAuthor titleLabel.setText(text: title) - authorLabel.setText(text: wrappedAuthor) - publisherLabel.setText(text: publisher) + authorLabel.setText(text: wrappedAuthor) + publisherLabel.setText(text: normalizedPublisher) + separatorLabel.isHidden = wrappedAuthor.isEmpty || normalizedPublisher.isEmpty문자열 리터럴(“저자 정보 없음”)은 로컬라이제이션 자원으로 이관하는 것도 고려 바랍니다.
src/Projects/BKPresentation/Sources/MainFlow/Search/Coordinator/SearchCoordinator.swift (1)
50-59: 도서 상세 전환 흐름 추가: 구현은 명확합니다 — child coordinator 정리 루틴 확인BookDetailCoordinator 생성→child 추가→start의 플로우는 타당합니다. 다만 push로 진행된 상세 화면 종료 시 childCoordinators에서 제거되는 경로가 준비되어 있는지 확인 부탁드립니다(메모리/중복 start 방지).
- 보유 중인 공통 패턴(예: Coordinator.removeChild(_:)) 또는 delegate/closure(onFinish)를 통해 pop 시점에 self.removeChildCoordinator(...) 호출 권장.
- 메서드 인자명(userBookId)이 도메인에서 사용하는 bookId와 혼용되지 않도록 명확한 네이밍 합의도 체크해 주세요.
src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift (1)
93-102: 탭 폭주 방지 throttle(800ms) 적용은 OK — latest 전략/시간값 재검토 제안latest: false로 첫 탭만 처리합니다. 사용자가 800ms 내 재탭하면 무시됩니다. 상세 화면 네비게이션 UX 측면에서
- latest: true로 “마지막 탭”을 반영하거나,
- 윈도를 500ms로 단축하거나,
- 버튼 단위로 isEnabled 토글(네비게이션 중 비활성)로 보완
을 고려해 보세요. 현재도 큰 문제는 없어 선택 사항입니다.
src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchResultCell.swift (1)
52-56: 라이브러리 결과 스타일 분기 로직: 의도는 명확 — UX 톤과의 정합성 점검 권장isLibraryBook && recordCount != nil → .record, isLibraryBook → .alreadyEnroll 분기는 “내 서재 결과”에서 기록 유무에 따른 뱃지/문구 노출을 잘 표현합니다. 다만 .alreadyEnroll 스타일은 배경/텍스트를 disable 톤으로 처리하여(디자인 컴포넌트 구현상) “탭 불가” 인상을 줄 수 있습니다. 현재 canSelect=true로 탭 이동을 허용하므로, 시각적 affordance와 상충할 수 있습니다.
옵션:
- 라이브러리 전용의 selectable 이미-등록 스타일(색상만 유지/감쇠 제거) 신설
- 또는 isLibraryBook && recordCount == nil일 때도 .regular을 사용하고 “이미 등록됨” 문구만 extraLabel로 표시
로직 자체는 간결합니다만, 가독성을 위해 분기를 함수로 추출하는 것도 고려해 주세요.
- let style: BKBookSummaryViewStyle = { - if isLibraryBook && recordCount != nil { - return .record - } else if isLibraryBook { - return .alreadyEnroll - } else if recordCount != nil { - return .record - } else if canSelect { - return .regular - } else { - return .alreadyEnroll - } - }() + let style: BKBookSummaryViewStyle = { + if isLibraryBook { + return (recordCount != nil) ? .record : .alreadyEnroll + } + if recordCount != nil { return .record } + return canSelect ? .regular : .alreadyEnroll + }()src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift (1)
219-245: 라이브러리 결과 셀 구성 적절. 스타일 플래그 적용이 명확합니다.
- isLibraryBook: true와 recordCount 유무로 BKBookSummaryView 스타일이 안정적으로 선택됩니다.
- 셀 탭 가능(canSelect: true) 설정도 요구사항과 맞습니다.
선택사항:
- 이미 등록된 항목에 대해 extraText(예: "내 서재") 등의 배지를 노출하고 싶다면 SearchResultCell.configure의 extraText 매개변수를 활용하는 방안을 고려할 수 있습니다.
src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift (1)
11-12: 결과 모델 구조 확장(.libraryResult, ResultState에 bookInfos 추가) — 현재 요구에는 적합합니다. 구조 단순화는 선택사항.
- View에서 두 경로(Book, BookInfo)를 동시에 처리하기 위해 병렬 배열(books/bookInfos)을 둔 점은 이해됩니다.
- 다만 장기적으로는 ResultState가 [SearchItem] 하나로만 스냅샷 소스에 직접 대응하도록 단순화하면 중복 분기/초기화가 줄어듭니다. 이번 PR 범위를 넘는 개선이므로 선택적으로 고려해 주세요.
Also applies to: 20-34
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (8)
src/Projects/BKDesign/Sources/Components/Summary/BKBookSummaryView.swift(2 hunks)src/Projects/BKDomain/Sources/Entity/BookInfo.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Search/Coordinator/SearchCoordinator.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchResultCell.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift(5 hunks)src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift(3 hunks)src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift(9 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/View/SettingViewController.swift(0 hunks)
💤 Files with no reviewable changes (1)
- src/Projects/BKPresentation/Sources/MainFlow/Setting/View/SettingViewController.swift
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-11T12:49:16.942Z
Learnt from: doyeonk429
PR: YAPP-Github/Reed-iOS#169
File: src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchResultCell.swift:31-41
Timestamp: 2025-08-11T12:49:16.942Z
Learning: In Reed-iOS project's SearchResultCell, a new BKBookSummaryView instance is created each time configure() is called rather than reusing the existing view. The old view is properly removed and set to nil in prepareForReuse(), allowing deinitialization to handle all cleanup including image downloads. Calling clearView() before removal is unnecessary in this pattern.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchResultCell.swiftsrc/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift
📚 Learning: 2025-07-11T12:12:48.911Z
Learnt from: clxxrlove
PR: YAPP-Github/Reed-iOS#79
File: src/Projects/BKPresentation/Sources/MainFlow/Setting/ViewModel/SettingViewModel.swift:78-85
Timestamp: 2025-07-11T12:12:48.911Z
Learning: AppVersionUseCase.execute() returns AnyPublisher<String, Never> where the Failure type is Never, making error handling unnecessary. The app version is retrieved from the main bundle which is always available, and the implementation already handles missing version by defaulting to "-".
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift
🧬 Code graph analysis (5)
src/Projects/BKDesign/Sources/Components/Summary/BKBookSummaryView.swift (1)
src/Projects/BKDesign/Sources/Components/Label/BKLabel.swift (1)
setText(109-113)
src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift (2)
src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift (1)
send(172-180)src/Projects/BKPresentation/Sources/MainFlow/Search/Coordinator/SearchCoordinator.swift (1)
didTapBookDetail(50-59)
src/Projects/BKPresentation/Sources/MainFlow/Search/Coordinator/SearchCoordinator.swift (2)
src/Projects/BKPresentation/Sources/Common/Coordinator/Coordinator.swift (1)
addChildCoordinator(47-52)src/Projects/BKPresentation/Sources/MainFlow/BookDetail/Coordinator/BookDetailCoordinator.swift (1)
start(25-34)
src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift (2)
src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchResultCell.swift (1)
configure(43-82)src/Projects/BKDesign/Sources/Components/Summary/BKBookSummaryView.swift (1)
configure(240-283)
src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift (1)
src/Projects/BKData/Sources/Repository/DefaultBookRepository.swift (1)
search(15-40)
🔇 Additional comments (9)
src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift (2)
13-13: 이벤트 추가 자체는 적절합니다검색 셀에서 상세 이동을 분리된 이벤트로 명시한 점이 명확하고 추후 테스트/트래킹도 수월합니다.
44-45: onAppearWithoutReset 전환: 상태 유지 의도 확인 필요화면 복귀 시 기존 검색 상태를 보존하려는 의도로 보입니다. 초기화가 필요했던 경로(첫 진입, 탭 재선택 등)와의 동작 차이를 ViewModel 단에서 충분히 커버하는지 확인해 주세요.
src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchResultCell.swift (1)
48-50: API 확장은 안전합니다기본값을 부여해 기존 호출부 호환성을 유지했습니다. isLibraryBook 플래그 추가로 스타일 분기가 명확해졌습니다.
src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift (2)
146-148: DataSource에 .libraryResult 분기 추가 — 좋습니다.셀 타입 분기가 명확하고, 재사용 셀(SearchResultCell)을 활용해 구현 일관성이 유지됩니다.
103-116: count 전달 흐름 추가 확인 필요SearchViewController에서 contentView.applySnapshot 호출부는
snapshot.count를 count 인자로 넘기고 있음이 확인되었습니다.
다음 두 가지를 검증해 주세요:
- snapshot.count가 ViewModel의
totalResults값을 그대로 전달하는지 확인
(ViewModel에서 스냅샷 생성 시state.totalResults를 그대로 사용하고 있는지)SearchState.totalResults에 실제 결과 건수가 올바르게 세팅되는지 확인확인 위치:
- src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift
• applySnapshot 호출부 (134–136번 줄)- src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift
• applySnapshot 정의 (85–88번 줄)src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift (4)
99-99: onAppearWithoutReset 추가 — UX 관점에서 적절합니다.검색 결과 화면 복귀 시 목록을 보존하고, 최근 검색은 초기 상태에서만 로드하도록 한 분리가 명확합니다.
Also applies to: 192-198
217-217: 기본 검색 결과 수신 시 allBookInfos 초기화 — 충돌 방지에 필요합니다.기본 검색과 내 서재 검색의 소스 혼재를 예방하는 안전한 처리입니다.
228-240: 내 서재 검색 결과 수신 처리 — 상태 전환과 페이지 가능 여부 계산 적절.bookInfos로 ResultState를 구성하고 totalResults/hasMoreData를 일관되게 갱신합니다.
336-371: 리뷰 코멘트 무시:myLibrarySearch도 1-based 인덱스를 사용합니다.
- SearchViewModel.swift (132행)에서
private var currentPage = 1로 선언되어 있어,.myLibrarySearch분기에서도 첫 페이지가 1로 전달됩니다.MyLibrarySearchBookUseCase.execute(query:startIndex:)의startIndex값은MyLibraryParameters.pageNumber로 바로 매핑되며,LibraryRequestDTO(pageNumber:)를 통해page파라미터로 전송됩니다.- 따라서 “myLibrary가 0-based로 시작”한다는 가정은 코드상 맞지 않습니다.
Likely an incorrect or invalid review comment.
| import Foundation | ||
|
|
||
| public struct BookInfo: Equatable { | ||
| public struct BookInfo: Equatable, Hashable { |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Hashable 합성은 모든 필드를 포함합니다 — DiffableDataSource 식별자 안정성 보장 필요
BookInfo를 Hashable로 확장한 목적이 DiffableDataSource의 item 식별자로 쓰기 위함이라면, 현재의 자동 합성(Hashable/Equatable)은 모든 저장 프로퍼티를 포함합니다. 이는 recordCount/updatedAt 등 변동 가능한 값이 바뀔 때마다 “다른 아이템”으로 간주되어 불필요한 삭제/삽입 애니메이션, 셀 깜빡임, 스크롤 위치 튐 등이 발생할 수 있습니다. 스냅샷 식별자는 변하지 않는 키(예: bookId)로 고정하는 것이 안전합니다.
권장(둘 중 택1):
- 방법 A: BookInfo의 동치성과 해시를 bookId(또는 bookId+isbn) 기준으로 재정의
- 방법 B: Diffable에 사용할 전용 Item(예: BookInfoItem: Hashable)을 만들어 id만으로 식별하고, 셀 구성에는 원본 BookInfo를 보관
방법 A 예시:
public struct BookInfo: Equatable, Hashable {
@@
public let recordCount: Int
@@
) {
self.bookId = bookId
@@
}
+
+ // id 기반으로 동치성/해시를 고정해 스냅샷 안정성 확보
+ public static func == (lhs: BookInfo, rhs: BookInfo) -> Bool {
+ return lhs.bookId == rhs.bookId
+ }
+
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(bookId)
+ }참고: 기존에 BookInfo의 Equatable(자동 합성)을 “내용 비교”로 사용하던 코드가 있다면 의미가 바뀌므로 영향 범위를 점검하세요. 그 경우 방법 B가 더 안전합니다.
또한 BookStatus가 Hashable을 만족하는지 확인해 주세요(URL/Date는 Hashable입니다).
다음 스크립트로 BookStatus 선언/프로토콜을 빠르게 확인할 수 있어요.
🏁 Script executed:
#!/bin/bash
# BookStatus 선언 위치와 프로토콜 확인
rg -n -C2 -g 'src/**' -P '^(public\s+)?(enum|struct|class)\s+BookStatus\b'
# Hashable/Equatable 채택 여부
rg -n -C1 -g 'src/**' -P 'BookStatus\s*:\s*[^\\{]*\bHashable\b'Length of output: 1028
안정적인 DiffableDataSource 식별자 확보를 위해 BookInfo의 Hashable/Equatable 커스텀 구현 필요
-
문제:
public struct BookInfo: Equatable, Hashable의 자동 합성은bookId외의 모든 저장 프로퍼티(recordCount, updatedAt 등)를 포함합니다. 이 값들이 변경되면 DiffableDataSource가 “다른 아이템”으로 간주하여 삽입/삭제 애니메이션, 셀 깜빡임, 스크롤 위치 튐 등이 발생할 수 있습니다. -
해결 방안 (둘 중 택1):
- 방법 A: BookInfo 내에서
==와hash(into:)를bookId기준으로만 재정의public struct BookInfo: Equatable, Hashable { @@ // id 기반으로 동치성/해시를 고정해 스냅샷 안정성 확보 public static func == (lhs: BookInfo, rhs: BookInfo) -> Bool { return lhs.bookId == rhs.bookId } public func hash(into hasher: inout Hasher) { hasher.combine(bookId) } } - 방법 B: DiffableDataSource 전용 식별자 타입(
BookInfoItem: Hashable)을 정의하여 식별은id만으로 처리하고, 셀 구성에는 원본BookInfo를 보관
- 방법 A: BookInfo 내에서
-
추가 확인 사항:
- 기존에
BookInfo의 자동 Equatable/Hashable을 “내용 비교” 용도로 사용하던 코드가 있다면, 동작 변경에 따른 영향 범위를 반드시 점검해주세요. BookInfo가 사용하는BookStatus는src/Projects/BKDomain/Sources/VO/BookStatus.swift에로 선언되어 있으며 현재public enum BookStatus: String, Codable { … }
Hashable채택이 없습니다.
• 자동 합성을 유지하려면BookStatus에도Hashable채택을 추가하거나
• 방법 A를 선택하여BookStatus를 해시 계산에서 제외해도 무방합니다.
- 기존에
🤖 Prompt for AI Agents
In src/Projects/BKDomain/Sources/Entity/BookInfo.swift around line 5, the
automatic Equatable/Hashable synthesis includes all stored properties which
makes DiffableDataSource treat items as different when non-identifier fields
change; fix by either (A) overriding == and hash(into:) on BookInfo to use only
bookId for equality/hash (so DiffableDataSource identity is stable) or (B)
create a separate DiffableDataSource identifier type (e.g., BookInfoItem:
Hashable) that contains only the id and use that as the item identifier while
keeping the original BookInfo for cell configuration; also check for any code
relying on full-value equality and, if keeping synthesis, add Hashable to
BookStatus or exclude it from hashing per the chosen approach.
|
|
||
| switch item { | ||
| case .result(let book): | ||
| switch book.userBookStatus { | ||
| case .beforeRegistration, nil: | ||
| eventPublisher.send(.upsertBook(book.isbn)) | ||
| default: | ||
| break | ||
| } | ||
|
|
||
| case .libraryResult(let bookInfo): | ||
| eventPublisher.send(.goToBookDetail(isbn: bookInfo.isbn, userBookId: bookInfo.bookId)) | ||
|
|
||
| case .query: break | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
탭 액션 분기(기본: 업서트, 내 서재: 상세 이동) 구성은 요구사항에 부합합니다. 업서트 status 매핑만 확인 필요.
- .result(book)에서만 업서트를 보내고, .libraryResult(bookInfo)는 상세로 이동시키는 분리는 자연스럽습니다.
- 다만 eventPublisher.send(.upsertBook(book.isbn))에서 status 인자가 없는 이벤트가 컨트롤러에서 ViewModel.Action.upsertBook(isbn:status:)로 정상 매핑되는지 확인해 주세요. 기본 상태값이 무엇인지도 함께 점검을 권장합니다.
다음 스크립트로 이벤트 → 액션 매핑을 확인해 주세요.
🏁 Script executed:
#!/bin/bash
# SearchViewEvent 정의와 컨트롤러 매핑 확인
rg -nP -C3 'enum\s+SearchViewEvent\b|case\s+upsertBook|case\s+goToBookDetail' --type=swift
rg -nP -C3 'didReceive\(event:|bind\(|eventPublisher' --type=swiftLength of output: 47247
기본 상태값 확인을 위해 SearchViewModel.Action 정의를 살펴보았습니다.
enum Action내부에case upsertBook(isbn: String, status: BookRegistrationStatus)만 선언되어 있으며, 기본값을 지정한 오버로드나 편의 생성자가 없습니다.SearchViewController에서는.upsertBook(isbn: isbn)형태로 status 인자를 전달하지 않고 호출하고 있어 컴파일 오류가 발생합니다.
따라서, 아래 중 하나를 적용해야 합니다:
- Action enum에 기본 status 제공
enum Action { case upsertBook(isbn: String, status: BookRegistrationStatus = .beforeRegistration) // ... }
- SearchViewController에서 명시적 status 전달
.sink { [weak self] isbn in self?.viewModel.send(.upsertBook(isbn: isbn, status: .beforeRegistration)) } - 편의 메서드 추가
extension SearchViewModel.Action { static func upsertBook(_ isbn: String) -> Self { .upsertBook(isbn: isbn, status: .beforeRegistration) } }
위 중 한 가지 방안을 적용하여 status 매핑을 보장해 주세요.
🤖 Prompt for AI Agents
In src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift
around lines 322-336 the call eventPublisher.send(.upsertBook(book.isbn)) no
longer matches SearchViewModel.Action which defines upsertBook(isbn: String,
status: BookRegistrationStatus) causing a compile error; fix by either (A)
adding a default parameter to the Action case so it can be called with only
isbn, (B) adding a convenience factory/extension on SearchViewModel.Action like
static func upsertBook(_ isbn: String) -> Self that supplies a default status,
or (C) update this call site to pass an explicit status value (e.g.
.beforeRegistration); choose one approach and apply it consistently across
usages.
| if searchViewType == .myLibrarySearch { | ||
| return myLibrarySearchUseCase.execute( | ||
| query: query, | ||
| startIndex: currentPage | ||
| ) | ||
| .map { result in | ||
| let books = result.books.map(self.mapBookInfoToBook) | ||
| return Action.fetchNextPageSuccessed(books) | ||
| } | ||
| .catch { [weak self] in | ||
| self?.lastEffect = .loadNextPage | ||
| return Just(Action.errorOccured($0)) | ||
| } | ||
| .eraseToAnyPublisher() | ||
| } else { | ||
| return defaultSearchUseCase.execute( | ||
| query: query, | ||
| startIndex: currentPage, | ||
| isGuestMode: AccessModeCenter.shared.mode.value == .guest | ||
| ) | ||
| .map { Action.fetchNextPageSuccessed($0.books) } | ||
| .catch { [weak self] in | ||
| self?.lastEffect = .loadNextPage | ||
| return Just(Action.errorOccured($0)) | ||
| } | ||
| .eraseToAnyPublisher() | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
내 서재 다음 페이지가 Book으로 변환되어 UI/동작이 바뀌는 버그
문제:
- myLibrarySearch의 loadNextPage에서 BookInfo를 Book으로 변환(mapBookInfoToBook)한 뒤 Action.fetchNextPageSuccessed로 내려보냅니다.
- reducer의 .fetchNextPageSuccessed는 books만 갱신하고, View는 .result(Book)로 렌더링합니다.
- 결과적으로 첫 페이지는 .libraryResult(BookInfo) 셀(상세 이동), 다음 페이지부터는 .result(Book) 셀(업서트 시도)로 섞여 UI/행동이 바뀝니다.
해결 지향:
- 라이브러리 경로는 페이지네이션 전 과정에서 BookInfo를 유지해야 합니다.
- 별도의 액션(.fetchLibraryNextPageSuccessed([BookInfo]))을 도입하고, reducer에서 allBookInfos를 append하도록 수정하세요.
패치(1/2): 액션 추가
enum Action {
case onAppear
case onAppearWithoutReset // 검색 결과 유지한 채로 appear
case search(String)
case loadNextPage
case loadNoteFlow
case deleteRecentQuery(String)
case upsertBook(isbn: String, status: BookRegistrationStatus)
case fetchRecentQueriesSuccessed([String])
case fetchSearchResultSuccessed((books: [Book], totalResults: Int))
case fetchLibrarySearchResultSuccessed((bookInfos: [BookInfo], totalResults: Int))
case fetchNextPageSuccessed([Book])
+ case fetchLibraryNextPageSuccessed([BookInfo])
case upsertBookSuccessed(isbn: String, bookId: String)
case noteSuggestionShown
case errorOccured(DomainError)
case errorHandled
case retryTapped
case lastRetryTapped
}패치(2/2): myLibrary 페이지네이션에서 BookInfo를 유지
case .loadNextPage:
guard let query = currentQuery else {
return Empty().eraseToAnyPublisher()
}
- if searchViewType == .myLibrarySearch {
- return myLibrarySearchUseCase.execute(
- query: query,
- startIndex: currentPage
- )
- .map { result in
- let books = result.books.map(self.mapBookInfoToBook)
- return Action.fetchNextPageSuccessed(books)
- }
- .catch { [weak self] in
- self?.lastEffect = .loadNextPage
- return Just(Action.errorOccured($0))
- }
- .eraseToAnyPublisher()
- } else {
+ if searchViewType == .myLibrarySearch {
+ return myLibrarySearchUseCase.execute(
+ query: query,
+ startIndex: currentPage
+ )
+ .map { result in
+ // NOTE: result의 프로퍼티명이 구현에 따라 books 또는 bookInfos일 수 있습니다.
+ // BookInfo 배열을 반환하도록 맞춰 주세요.
+ let infos = result.bookInfos
+ return Action.fetchLibraryNextPageSuccessed(infos)
+ }
+ .catch { [weak self] in
+ self?.lastEffect = .loadNextPage
+ return Just(Action.errorOccured($0))
+ }
+ .eraseToAnyPublisher()
+ } else {
return defaultSearchUseCase.execute(
query: query,
startIndex: currentPage,
isGuestMode: AccessModeCenter.shared.mode.value == .guest
)
.map { Action.fetchNextPageSuccessed($0.books) }
.catch { [weak self] in
self?.lastEffect = .loadNextPage
return Just(Action.errorOccured($0))
}
.eraseToAnyPublisher()
}추가로 reducer에 다음 케이스를 추가해야 합니다(참고용 코드 스니펫):
// reducer switch 내부 어딘가(예: fetchNextPageSuccessed 바로 아래)
case .fetchLibraryNextPageSuccessed(let infos):
let unique = infos.filter { info in
!allBookInfos.contains { $0.isbn == info.isbn }
}
allBookInfos += unique
newState.isLoading = false
newState.searchState = .result(
ResultState(
bookInfos: allBookInfos,
placeholder: searchViewType.resultPlaceholder
)
)
newState.hasMoreData = allBookInfos.count < newState.totalResults위 수정으로 라이브러리 검색의 1페이지와 이후 페이지가 동일한 셀/동작(.libraryResult → 상세 이동)로 유지됩니다.
원하시면 위 변경을 반영한 전체 패치(PR)와 뷰모델 단위 테스트(라이브러리 페이지네이션 시 BookInfo 유지 여부 검증)까지 함께 준비해 드리겠습니다.
🤖 Prompt for AI Agents
src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift
around lines 377-403: the myLibrary pagination branch maps BookInfo to Book
(mapBookInfoToBook) and emits Action.fetchNextPageSuccessed, causing later pages
to render as Book cells; instead keep BookInfo across pagination by emitting a
new action (e.g. .fetchLibraryNextPageSuccessed([BookInfo])) from this branch
(do not map to Book), preserve lastEffect on error as currently done, and
eraseToAnyPublisher as before; update the rest of the codebase (actions enum and
reducer) to handle .fetchLibraryNextPageSuccessed by appending unique BookInfo
entries to allBookInfos and updating searchState/hasMoreData per the provided
reducer snippet.
|
게스트로 검색 접근 후 로그인 모달창 dismiss되면 최근검색어 셀의 높이가 길어지는 버그가 간헐적으로 발생한다고 디자인 파트에서 버그 리포트해주셨는데 여유 되시면 검색 쪽 작업하시면서 이것도 확인 부탁드립니다 |
|
@doyeonk429 네~ |
🔗 관련 이슈
📘 작업 유형
📙 작업 내역
🧪 테스트 내역
🎨 스크린샷 또는 시연 영상 (선택)
Simulator.Screen.Recording.-.iPhone.SE.3rd.generation.-.2025-08-21.at.18.46.50.mp4
Summary by CodeRabbit