Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4a00617
style/#126: View 컨벤션 적용 및 접근제어자 적용
0Hooni Apr 27, 2025
2d75d3b
refactor/#126: 목적이 잘 들어나도록 변수명 수정
0Hooni Apr 29, 2025
b593626
fix/#126: 불필요한 마지막 이벤트 추가 방출 제거
0Hooni Apr 29, 2025
1c72dd3
refactor/#126: Action의 네이밍을 Subject의 의도와 통일시킴
0Hooni Apr 29, 2025
7d82d2a
fix/#126: 불필요한 View 갱신을 막고 로직을 단순화시킴
0Hooni Apr 29, 2025
5996f6e
refactor/#126: 가독성 개선 및 변수명 오타 수정
0Hooni Apr 29, 2025
b85b113
fix/#126: 한번의 로딩에 너무 많은 아이템을 요청하는 부분을 수정
0Hooni Apr 29, 2025
6d0b611
refactor/#126: 반환되는 타입을 명시해줌
0Hooni Apr 29, 2025
6497a30
refactor/#126: 새 아이템을 중복으로 생성해주는 코드를 개선
0Hooni Apr 29, 2025
d4c7a81
refactor/#126: 새로 생성되는 Cell.Input임을 명시하도록 수정
0Hooni Apr 29, 2025
0b57cd5
refactor/#126: 첫 페이지인 경우에만 filterTitle이 설정되도록 수정
0Hooni May 1, 2025
00f5342
fix/#126: 첫 페이지가 아닐때는 reload가 아닌 update로 동작되도록 수정
0Hooni May 1, 2025
7ccb36d
feat/#126: 새 아이템만 업데이트 할 수 있도록 별도의 mutate와 action 추가
0Hooni May 1, 2025
a53ff06
feat/#126: 뷰가 다시 그려질때는 관리하던 페이지네이션 정보 초기화
0Hooni May 1, 2025
3ae769e
feat/#126: 새로운 updateBottomSearchList mutation 동작 추가
0Hooni May 1, 2025
7574d2b
refactor/#126: 로직 개선 및 이해를 위한 주석 추가
0Hooni May 1, 2025
904830f
fix/#126: reloadData가 새로운 상태로 그려져야될때만 동작하도록 수정
0Hooni May 1, 2025
57e7a84
feat/#126: 새로운 페이지네이션을 동작하기 위한 controller bind 추가
0Hooni May 1, 2025
12bbf10
style/#126: 오타 수정
0Hooni May 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Contributor

Choose a reason for hiding this comment

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

configureIUI와 addViews에서 뷰추가가 중복되있는것 같습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

@zzangzzangguy

앗! 저는 재사용 셀 등록은 뷰를 추가하는 과정은 아니라고 생각해서 configureUI에 등록을 해뒀던 거였습니다 👍🏻

이 부분은 생각의 차이가 있을 수 있어서 다음 회의때 어떤 스타일로 사용할건지 의논하고 하나의 스타일로 병합하면 좋을것같아요!

Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@ final class SearchController: BaseViewController, View {
private var mainView = SearchView()
private var sections: [any Sectionable] = []
private let cellTapped: PublishSubject<IndexPath> = .init()
private let pageChange: PublishSubject<Void> = .init()
private let loadNextPage = PublishSubject<Void>()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

변수명을 더 직관적으로 변경하셨군요 😂


// MARK: - Life Cycle
extension SearchController {
override func viewDidLoad() {
super.viewDidLoad()
setUp()

self.addViews()
self.setupContstraints()
self.configureUI()
}

override func viewWillAppear(_ animated: Bool) {
Expand All @@ -41,36 +44,50 @@ extension SearchController {

// MARK: - SetUp
private extension SearchController {
func setUp() {
func addViews() {
[mainView].forEach {
self.view.addSubview($0)
}
}

func setupContstraints() {
mainView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
}

func configureUI() {
if let layout = reactor?.compositionalLayout {
mainView.contentCollectionView.collectionViewLayout = layout
}

mainView.contentCollectionView.delegate = self
mainView.contentCollectionView.dataSource = self

mainView.contentCollectionView.register(
SearchTitleSectionCell.self,
forCellWithReuseIdentifier: SearchTitleSectionCell.identifiers
)

mainView.contentCollectionView.register(
SpacingSectionCell.self,
forCellWithReuseIdentifier: SpacingSectionCell.identifiers
)

mainView.contentCollectionView.register(
CancelableTagSectionCell.self,
forCellWithReuseIdentifier: CancelableTagSectionCell.identifiers
)

mainView.contentCollectionView.register(
SearchCountTitleSectionCell.self,
forCellWithReuseIdentifier: SearchCountTitleSectionCell.identifiers
)

mainView.contentCollectionView.register(
HomeCardSectionCell.self,
forCellWithReuseIdentifier: HomeCardSectionCell.identifiers
)
view.addSubview(mainView)
mainView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
}
}

Expand All @@ -90,19 +107,45 @@ extension SearchController {
.bind(to: reactor.action)
.disposed(by: disposeBag)

pageChange
.throttle(.milliseconds(1000), scheduler: MainScheduler.asyncInstance)
.map { Reactor.Action.changePage }
loadNextPage
.throttle(.seconds(1), latest: false, scheduler: MainScheduler.asyncInstance)
.map { Reactor.Action.loadNextPage }
.bind(to: reactor.action)
.disposed(by: disposeBag)

reactor.state
.filter { $0.newBottomSearchList.isEmpty && $0.bottomSearchListLastIndexPath == nil }
.withUnretained(self)
.subscribe { (owner, state) in
.subscribe { owner, state in
owner.sections = state.sections
owner.mainView.contentCollectionView.reloadData()
}
.disposed(by: disposeBag)

reactor.state
.map { (sections: $0.sections,
newItems: $0.newBottomSearchList,
indexPath: $0.bottomSearchListLastIndexPath) }
.filter { !$0.newItems.isEmpty && $0.indexPath != nil }
.withUnretained(self)
.subscribe { (owner, subscribeResponse) in
let (updatedSections, newPopUpItems, popUpGridindexPath) = subscribeResponse
guard let popUpGridindexPath = popUpGridindexPath else { return }

let start = popUpGridindexPath.item
let count = newPopUpItems.count
let section = popUpGridindexPath.section
let indexPaths = (start..<start+count).map {
IndexPath(item: $0, section: section)
}

owner.mainView.contentCollectionView.performBatchUpdates {
// 데이터 모델을 업데이트한 뒤 삽입
owner.sections = updatedSections
owner.mainView.contentCollectionView.insertItems(at: indexPaths)
}
}
.disposed(by: disposeBag)
}
}

Expand All @@ -122,8 +165,9 @@ extension SearchController: UICollectionViewDelegate, UICollectionViewDataSource
) -> UICollectionViewCell {
let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath)
guard let userDefaultService = reactor?.userDefaultService else { return cell }
var searchList = userDefaultService.fetchArray(key: "searchList") ?? []
let searchList = userDefaultService.fetchArray(key: "searchList") ?? []
guard let reactor = reactor else { return cell }

if let cell = cell as? SearchTitleSectionCell {
cell.titleButton.rx.tap
.map { Reactor.Action.recentSearchListAllDeleteButtonTapped }
Expand Down Expand Up @@ -168,6 +212,7 @@ extension SearchController: UICollectionViewDelegate, UICollectionViewDataSource
.bind(to: reactor.action)
.disposed(by: cell.disposeBag)
}

return cell
}

Expand All @@ -177,7 +222,7 @@ extension SearchController: UICollectionViewDelegate, UICollectionViewDataSource
let scrollViewHeight = scrollView.frame.size.height
let contentOffsetY = scrollView.contentOffset.y
if contentOffsetY + scrollViewHeight >= contentHeight {
pageChange.onNext(())
loadNextPage.onNext(())
Copy link
Member

Choose a reason for hiding this comment

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

스크롤 이벤트 포인트가 변경되지는 않았군요!!

Copy link
Member Author

@0Hooni 0Hooni May 1, 2025

Choose a reason for hiding this comment

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

아마 몇개셀 이전에 돌아가도록 하면 좋긴 하겠는데, 추후 새로운 피쳐로 재구성을 염두하고 있어서 그런지 불필요한 작업이라 판단해서 추후 작업할때 반영해볼것 같아요!

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ final class SearchReactor: Reactor {
case changeCategory(categoryList: [Int64], categoryTitleList: [String?])
case categoryDelteButtonTapped(indexPath: IndexPath)
case resetCategory
case changePage
case loadNextPage
case bookmarkButtonTapped(indexPath: IndexPath)
case resetSearchKeyWord
}
Expand All @@ -33,11 +33,29 @@ final class SearchReactor: Reactor {
case moveToDetailScene(controller: BaseViewController, indexPath: IndexPath)
case setSearchKeyWord(text: String?)
case resetSearchKeyWord
case updateBottomSearchList(newItems: [HomeCardSectionCell.Input], IndexPath: IndexPath)
}

struct State {
var sections: [any Sectionable] = []
var searchKeyWord: String?
var newBottomSearchList: [HomeCardSectionCell.Input] = []
var bottomSearchListLastIndexPath: IndexPath?

mutating func resetPaginationState() {
self.newBottomSearchList = []
self.bottomSearchListLastIndexPath = nil
}

mutating func updateBottomGridSection(by newItems: [HomeCardSectionCell.Input]) {
sections = sections.map { section in
if var grid = section as? HomeCardGridSection {
grid.inputDataList.append(contentsOf: newItems)
return grid
}
return section
}
}
}

// MARK: - properties
Expand Down Expand Up @@ -102,18 +120,11 @@ final class SearchReactor: Reactor {
switch action {
case .resetSearchKeyWord:
return Observable.just(.resetSearchKeyWord)
case .changePage:
if isLoading {
return Observable.just(.loadView)
} else {
if currentPage < lastPage {
isLoading = true
currentPage += 1
return setBottomSearchList(sort: sort)
} else {
return Observable.just(.loadView)
}
}
case .loadNextPage:
guard !isLoading, currentPage < lastPage else { return Observable.empty() }
isLoading = true
currentPage += 1
return setBottomSearchList(sort: sort)
case .viewWillAppear:
setSearchList()
return setBottomSearchList(sort: sort)
Expand Down Expand Up @@ -186,6 +197,7 @@ final class SearchReactor: Reactor {
switch mutation {
case .loadView:
newState.sections = getSection()
newState.resetPaginationState()
case .moveToCategoryScene(let controller):
let categoryIDList = searchCategorySection.inputDataList.compactMap { $0.id }
let nextController = SearchCategoryController()
Expand Down Expand Up @@ -236,6 +248,11 @@ final class SearchReactor: Reactor {
case .resetSearchKeyWord:
newState.searchKeyWord = nil
newState.sections = getSection()

case .updateBottomSearchList(let newItems, let indexPath):
newState.updateBottomGridSection(by: newItems)
newState.newBottomSearchList = newItems
newState.bottomSearchListLastIndexPath = indexPath
}
return newState
}
Expand Down Expand Up @@ -306,52 +323,69 @@ final class SearchReactor: Reactor {
}

func setBottomSearchList(sort: String?) -> Observable<Mutation> {
let isOpen = filterIndex == 0 ? true : false
let categorys = searchCategorySection.inputDataList.compactMap { $0.id }
return popUpAPIUseCase.getSearchBottomPopUpList(isOpen: isOpen, categories: categorys, page: currentPage, size: 50, sort: sort)
.withUnretained(self)
.map { (owner, response) in
let isLogin = response.loginYn
if owner.currentPage == 0 {
owner.searchListSection.inputDataList = response.popUpStoreList.map {
return .init(
imagePath: $0.mainImageUrl,
id: $0.id,
category: $0.category,
title: $0.name,
address: $0.address,
startDate: $0.startDate,
endDate: $0.endDate,
isBookmark: $0.bookmarkYn,
isLogin: isLogin
)
}
} else {
if owner.currentPage != owner.lastAppendPage {
owner.lastAppendPage = owner.currentPage
let newData = response.popUpStoreList.map {
return HomeCardSectionCell.Input(
imagePath: $0.mainImageUrl,
id: $0.id,
category: $0.category,
title: $0.name,
address: $0.address,
startDate: $0.startDate,
endDate: $0.endDate,
isBookmark: $0.bookmarkYn,
isLogin: isLogin
)
}
owner.searchListSection.inputDataList.append(contentsOf: newData)
}
}
let isOpen = filterIndex == 0
let categories = searchCategorySection.inputDataList.compactMap { $0.id }

return popUpAPIUseCase.getSearchBottomPopUpList(
isOpen: isOpen,
categories: categories,
page: currentPage,
size: 10,
sort: sort
)
.withUnretained(self)
.map { (owner, response) in
// 1) 새로 받아오기 전의 기존 아이템 개수 저장
let previousCount = owner.searchListSection.inputDataList.count

// 2) API 결과 매핑
let newItems = response.popUpStoreList.map {
HomeCardSectionCell.Input(
imagePath: $0.mainImageUrl,
id: $0.id,
category: $0.category,
title: $0.name,
address: $0.address,
startDate: $0.startDate,
endDate: $0.endDate,
isBookmark: $0.bookmarkYn,
isLogin: response.loginYn
)
}

// 3) 첫 페이지 vs 이후 페이지 분기
if owner.currentPage == 0 {
// 첫 페이지는 전체 reload
// SearchCountTitleSection 설정
let isOpenString = isOpen ? "오픈・" : "종료・"
let sortedString = owner.sortedIndex == 0 ? "신규순" : "인기순"
let sortedTitle = isOpenString + sortedString
owner.searchSortedSection.inputDataList = [.init(count: response.totalElements, sortedTitle: sortedTitle)]
owner.searchSortedSection.inputDataList = [
SearchCountTitleSectionCell.Input(
count: response.totalElements,
sortedTitle: sortedTitle
)
]
owner.searchListSection.inputDataList = newItems
owner.lastAppendPage = owner.currentPage
owner.lastPage = response.totalPages
owner.isLoading = false
return .loadView
} else {
// 다음 페이지는 append 후 부분 업데이트
owner.lastAppendPage = owner.currentPage
owner.searchListSection.inputDataList.append(contentsOf: newItems)
owner.lastPage = response.totalPages
owner.isLoading = false

// HomeCardGridSection이 컬렉션뷰에서 몇 번째 섹션인지 계산
let sectionIndex = owner.getSection().enumerated()
.first { _, section in section is HomeCardGridSection }!.offset

// append된 첫 아이템의 IndexPath
let firstIndexPath = IndexPath(item: previousCount, section: sectionIndex)
return .updateBottomSearchList(newItems: newItems, IndexPath: firstIndexPath)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,31 @@ import SnapKit
final class SearchView: UIView {

// MARK: - Components
let contentCollectionView: UICollectionView = {
return UICollectionView(frame: .zero, collectionViewLayout: .init())
}()
let contentCollectionView = UICollectionView(frame: .zero, collectionViewLayout: .init())

// MARK: - init
init() {
super.init(frame: .zero)
setUpConstraints()

self.addSubviews()
self.setUpConstraints()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
fatalError("\(#file), \(#function) Error")
}
}

// MARK: - SetUp
private extension SearchView {

func addSubviews() {
[contentCollectionView].forEach {
self.addSubview($0)
}
}

func setUpConstraints() {
self.addSubview(contentCollectionView)
contentCollectionView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(56)
make.leading.trailing.bottom.equalToSuperview()
Expand Down
Loading