diff --git a/Projects/App/Sources/MainTab/MainTabPath.swift b/Projects/App/Sources/MainTab/MainTabPath.swift index 6981bc32..ace6513b 100644 --- a/Projects/App/Sources/MainTab/MainTabPath.swift +++ b/Projects/App/Sources/MainTab/MainTabPath.swift @@ -106,7 +106,8 @@ public extension MainTabFeature { case let .path(.element(_, action: .카테고리상세(.delegate(.contentItemTapped(content))))), let .pokit(.delegate(.contentDetailTapped(content))), let .remind(.delegate(.링크상세(content))), - let .path(.element(_, action: .링크목록(.delegate(.링크상세(content: content))))): + let .path(.element(_, action: .링크목록(.delegate(.링크상세(content: content))))), + let .path(.element(_, action: .검색(.delegate(.linkCardTapped(content: content))))): // TODO: 링크상세 모델과 링크수정 모델 일치시키기 state.contentDetail = ContentDetailFeature.State(contentId: content.id) return .none @@ -116,7 +117,8 @@ public extension MainTabFeature { let .pokit(.delegate(.링크수정하기(id))), let .remind(.delegate(.링크수정(id))), let .path(.element(_, action: .카테고리상세(.delegate(.링크수정(id))))), - let .path(.element(_, action: .링크목록(.delegate(.링크수정(id))))): + let .path(.element(_, action: .링크목록(.delegate(.링크수정(id))))), + let .path(.element(_, action: .검색(.delegate(.링크수정(id))))): return .run { send in await send(.inner(.링크추가및수정이동(contentId: id))) } case let .contentDetail(.presented(.delegate(.컨텐츠_삭제_완료(contentId: id)))): diff --git a/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaults.swift b/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaults.swift index e48c5d4b..46fa0066 100644 --- a/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaults.swift +++ b/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaults.swift @@ -19,10 +19,13 @@ extension DependencyValues { public struct UserDefaultsClient { public var boolKey: @Sendable (UserDefaultsKey.BoolKey) -> Bool = { _ in false } public var stringKey: @Sendable (UserDefaultsKey.StringKey) -> String? = { _ in "" } + public var stringArrayKey: @Sendable (UserDefaultsKey.ArrayKey) -> [String]? = { _ in [] } public var removeBool: @Sendable (UserDefaultsKey.BoolKey) async -> Void public var removeString: @Sendable (UserDefaultsKey.StringKey) async -> Void + public var removeStringArray: @Sendable (UserDefaultsKey.ArrayKey) async -> Void public var setBool: @Sendable (Bool, UserDefaultsKey.BoolKey) async -> Void public var setString: @Sendable (String, UserDefaultsKey.StringKey) async -> Void + public var setStringArray: @Sendable ([String], UserDefaultsKey.ArrayKey) async -> Void } extension UserDefaultsClient: DependencyKey { @@ -32,12 +35,15 @@ extension UserDefaultsClient: DependencyKey { return Self( boolKey: { defaults().bool(forKey: $0.rawValue) }, stringKey: { defaults().string(forKey: $0.rawValue) }, + stringArrayKey: { defaults().stringArray(forKey: $0.rawValue) }, removeBool: { defaults().removeObject(forKey: $0.rawValue) }, removeString: { defaults().removeObject(forKey: $0.rawValue) }, + removeStringArray: { defaults().removeObject(forKey: $0.rawValue) }, setBool: { defaults().set($0, forKey: $1.rawValue) }, - setString: { defaults().set($0, forKey: $1.rawValue) } + setString: { defaults().set($0, forKey: $1.rawValue) }, + setStringArray: { defaults().set($0, forKey: $1.rawValue) } ) }() @@ -45,8 +51,10 @@ extension UserDefaultsClient: DependencyKey { Self( removeBool: { _ in }, removeString: { _ in }, + removeStringArray: { _ in }, setBool: { _, _ in }, - setString: { _, _ in } + setString: { _, _ in }, + setStringArray: {_, _ in } ) }() } diff --git a/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsKey.swift b/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsKey.swift index 5ce0452a..87616228 100644 --- a/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsKey.swift +++ b/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsKey.swift @@ -9,7 +9,7 @@ import Foundation public enum UserDefaultsKey { public enum BoolKey: String { - case doNothing + case autoSaveSearch } public enum StringKey: String { /// `구글` or `애플` @@ -17,4 +17,7 @@ public enum UserDefaultsKey { case authCode case jwt } + public enum ArrayKey: String { + case searchWords + } } diff --git a/Projects/CoreKit/Sources/Data/DTO/Base/BaseConditionRequest.swift b/Projects/CoreKit/Sources/Data/DTO/Base/BaseConditionRequest.swift index cfb61cfd..e6232492 100644 --- a/Projects/CoreKit/Sources/Data/DTO/Base/BaseConditionRequest.swift +++ b/Projects/CoreKit/Sources/Data/DTO/Base/BaseConditionRequest.swift @@ -8,19 +8,22 @@ import Foundation public struct BaseConditionRequest: Decodable { + public var searchWord: String public var categoryIds: [Int] public var isUnreadFiltered: Bool public var isFavoriteFlitered: Bool - public var startDate: Date? - public var endDate: Date? + public var startDate: String? + public var endDate: String? public init( + searchWord: String = "", categoryIds: [Int], isRead: Bool, favorites: Bool, - startDate: Date? = nil, - endDate: Date? = nil + startDate: String? = nil, + endDate: String? = nil ) { + self.searchWord = searchWord self.categoryIds = categoryIds self.isUnreadFiltered = isRead self.isFavoriteFlitered = favorites diff --git a/Projects/CoreKit/Sources/Data/Network/Category/CategoryEndpoint.swift b/Projects/CoreKit/Sources/Data/Network/Category/CategoryEndpoint.swift index e547eea9..b172877f 100644 --- a/Projects/CoreKit/Sources/Data/Network/Category/CategoryEndpoint.swift +++ b/Projects/CoreKit/Sources/Data/Network/Category/CategoryEndpoint.swift @@ -74,7 +74,7 @@ extension CategoryEndpoint: TargetType { parameters: [ "page": model.page, "size": model.size, - "sort": model.sort, + "sort": model.sort.map { String($0) }.joined(separator: ","), "filterUncategorized": categorized ], encoding: URLEncoding.default diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift index ebe57953..fdda7cd1 100644 --- a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift +++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift @@ -45,6 +45,10 @@ public struct ContentClient { public var 미분류_카테고리_컨텐츠_조회: @Sendable ( _ model: BasePageableRequest ) async throws -> ContentListInquiryResponse + public var 컨텐츠_검색: @Sendable ( + _ pageable: BasePageableRequest, + _ condition: BaseConditionRequest + ) async throws -> ContentListInquiryResponse } extension ContentClient: DependencyKey { @@ -81,6 +85,14 @@ extension ContentClient: DependencyKey { }, 미분류_카테고리_컨텐츠_조회: { model in try await provider.request(.미분류_카테고리_컨텐츠_조회(model: model)) + }, + 컨텐츠_검색: { pageable, condition in + try await provider.request( + .컨텐츠_검색( + pageable: pageable, + condition: condition + ) + ) } ) }() @@ -94,7 +106,8 @@ extension ContentClient: DependencyKey { 즐겨찾기: { _ in .mock }, 즐겨찾기_취소: { _ in .init() }, 카테고리_내_컨텐츠_목록_조회: { _, _, _ in .mock }, - 미분류_카테고리_컨텐츠_조회: { _ in .mock } + 미분류_카테고리_컨텐츠_조회: { _ in .mock }, + 컨텐츠_검색: { _, _ in .mock } ) }() } diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentEndpoint.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentEndpoint.swift index e01700e1..916b6a12 100644 --- a/Projects/CoreKit/Sources/Data/Network/Content/ContentEndpoint.swift +++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentEndpoint.swift @@ -23,6 +23,10 @@ public enum ContentEndpoint { condition: BaseConditionRequest ) case 미분류_카테고리_컨텐츠_조회(model: BasePageableRequest) + case 컨텐츠_검색( + pageable: BasePageableRequest, + condition: BaseConditionRequest + ) } extension ContentEndpoint: TargetType { @@ -48,6 +52,8 @@ extension ContentEndpoint: TargetType { return "/\(contentId)" case .미분류_카테고리_컨텐츠_조회: return "/uncategorized" + case .컨텐츠_검색(model: let model): + return "" } } @@ -66,7 +72,8 @@ extension ContentEndpoint: TargetType { return .patch case .카태고리_내_컨텐츠_목록_조회, - .미분류_카테고리_컨텐츠_조회: + .미분류_카테고리_컨텐츠_조회, + .컨텐츠_검색: return .get } } @@ -90,12 +97,11 @@ extension ContentEndpoint: TargetType { parameters: [ "page": pageable.page, "size": pageable.size, - "sort": pageable.sort, + "sort": pageable.sort.map { String($0) }.joined(separator: ","), "isRead": condition.isUnreadFiltered ? condition.isUnreadFiltered : "", "favorites": condition.isFavoriteFlitered ? condition.isFavoriteFlitered : "", "startDate": condition.startDate ?? "", - "endDate": condition.endDate ?? "", - "categoryIds": condition.categoryIds + "endDate": condition.endDate ?? "" ], encoding: URLEncoding.default ) @@ -104,7 +110,22 @@ extension ContentEndpoint: TargetType { parameters: [ "page": model.page, "size": model.size, - "sort": model.sort + "sort": model.sort.map { String($0) }.joined(separator: ",") + ], + encoding: URLEncoding.default + ) + case let .컨텐츠_검색(pageable, condition): + return .requestParameters( + parameters: [ + "page": pageable.page, + "size": pageable.size, + "sort": pageable.sort.map { String($0) }.joined(separator: ","), + "isRead": condition.isUnreadFiltered ? condition.isUnreadFiltered : "", + "favorites": condition.isFavoriteFlitered ? condition.isFavoriteFlitered : "", + "startDate": condition.startDate ?? "", + "endDate": condition.endDate ?? "", + "categoryIds": condition.categoryIds.map { String($0) }.joined(separator: ","), + "searchWord": condition.searchWord ], encoding: URLEncoding.default ) diff --git a/Projects/Domain/Sources/FilterBottom/FilterBottom.swift b/Projects/Domain/Sources/FilterBottom/FilterBottom.swift index b6b336ef..50b54d7c 100644 --- a/Projects/Domain/Sources/FilterBottom/FilterBottom.swift +++ b/Projects/Domain/Sources/FilterBottom/FilterBottom.swift @@ -8,15 +8,25 @@ import Foundation public struct FilterBottom: Equatable { + // - MARK: Response + /// 카테고리(포킷) 리스트 public var categoryList: BaseCategoryListInquiry + // - MARK: Request + /// 조회할 페이징 정보 + public var pageable: BasePageable + public init() { self.categoryList = .init( - data: [], page: 0, size: 10, sort: [], hasNext: false ) + self.pageable = .init( + page: 0, + size: 10, + sort: ["desc"] + ) } } diff --git a/Projects/Domain/Sources/Search/Search.swift b/Projects/Domain/Sources/Search/Search.swift index 793d0cfe..ccdc7ef3 100644 --- a/Projects/Domain/Sources/Search/Search.swift +++ b/Projects/Domain/Sources/Search/Search.swift @@ -14,6 +14,8 @@ public struct Search: Equatable { // - MARK: Request /// 검색 조건 public var condition: Condition + /// 조회할 페이징 정보 + public var pageable: BasePageable public init() { self.contentList = .init( @@ -26,9 +28,14 @@ public struct Search: Equatable { self.condition = .init( searchWord: "", categoryIds: [], - isRead: true, + isRead: false, favorites: false ) + self.pageable = .init( + page: 0, + size: 10, + sort: ["desc"] + ) } } diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift index 12000277..f0bb9ffe 100644 --- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift +++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift @@ -196,7 +196,7 @@ private extension CategoryDetailFeature { case .onAppear: return .run { send in - let request = BasePageableRequest(page: 0, size: 100, sort: ["desc"]) + let request = BasePageableRequest(page: 0, size: 100, sort: ["createdAt", "desc"]) let response = try await categoryClient.카테고리_목록_조회(request, true).toDomain() await send(.async(.카테고리_내_컨텐츠_목록_조회)) await send(.inner(.카테고리_목록_조회_결과(response))) diff --git a/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift b/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift index 6fba715a..b3b31187 100644 --- a/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift +++ b/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift @@ -217,8 +217,10 @@ private extension PokitRootFeature { if state.domain.categoryList.hasNext { return .run { [domain = state.domain.categoryList, sortType = state.sortType] send in - let sort = sortType == .sort(.최신순) ? "desc" : "asc" - let request = BasePageableRequest(page: domain.page + 1, size: 10, sort: [sort]) + let sort: [String] = sortType == .sort(.최신순) + ? ["createdAt", "desc"] + : ["name", "asc"] + let request = BasePageableRequest(page: domain.page + 1, size: 10, sort: sort) let classified = try await categoryClient.카테고리_목록_조회(request, true).toDomain() await send(.inner(.분류_페이지네이션_결과(contentList: classified))) } @@ -229,8 +231,10 @@ private extension PokitRootFeature { if state.domain.unclassifiedContentList.hasNext { return .run { [domain = state.domain.unclassifiedContentList, sortType = state.sortType] send in - let sort = sortType == .sort(.최신순) ? "desc" : "asc" - let request = BasePageableRequest(page: domain.page + 1, size: 10, sort: [sort]) + let sort: [String] = sortType == .sort(.최신순) + ? ["createdAt", "desc"] + : ["name", "asc"] + let request = BasePageableRequest(page: domain.page + 1, size: 10, sort: sort) let unclassified = try await contentClient.미분류_카테고리_컨텐츠_조회(request).toDomain() await send(.inner(.미분류_페이지네이션_결과(contentList: unclassified))) } @@ -316,8 +320,10 @@ private extension PokitRootFeature { contentList = state.domain.unclassifiedContentList, sortType = state.sortType ] send in - let sort = sortType == .sort(.최신순) ? "desc" : "asc" - let request = BasePageableRequest(page: 0, size: contentList.size, sort: [sort]) + let sort: [String] = sortType == .sort(.최신순) + ? ["createdAt", "desc"] + : ["name", "asc"] + let request = BasePageableRequest(page: 0, size: contentList.size, sort: sort) let contentList = try await contentClient.미분류_카테고리_컨텐츠_조회( request ).toDomain() @@ -326,8 +332,10 @@ private extension PokitRootFeature { case .목록조회_갱신용: return .run { [domain = state.domain.categoryList, sortType = state.sortType] send in - let sort = sortType == .sort(.최신순) ? "desc" : "asc" - let request = BasePageableRequest(page: 0, size: domain.size, sort: [sort]) + let sort: [String] = sortType == .sort(.최신순) + ? ["createdAt", "desc"] + : ["name", "asc"] + let request = BasePageableRequest(page: 0, size: domain.size, sort: sort) let classified = try await categoryClient.카테고리_목록_조회(request, true).toDomain() await send(.inner(.onAppearResult(classified: classified))) await send(.inner(.sort)) diff --git a/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchFeature.swift b/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchFeature.swift index c87df19c..fb102a55 100644 --- a/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchFeature.swift +++ b/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchFeature.swift @@ -15,8 +15,16 @@ import Util @Reducer public struct PokitSearchFeature { /// - Dependency - @Dependency(\.dismiss) var dismiss - @Dependency(\.pasteboard) var pasteboard + @Dependency(\.dismiss) + private var dismiss + @Dependency(\.mainQueue) + private var mainQueue + @Dependency(\.pasteboard) + private var pasteboard + @Dependency(\.userDefaults) + private var userDefaults + @Dependency(\.contentClient) + private var contentClient /// - State @ObservableState public struct State: Equatable { @@ -24,20 +32,7 @@ public struct PokitSearchFeature { @Presents var filterBottomSheet: FilterBottomFeature.State? - var searchText: String = "" - var recentSearchTexts: [String] = [ - "샤프 노트북", - "아이패드", - "맥북", - "LG 그램", - "LG 그램1", - "LG 그램2", - "LG 그램3", - "LG 그램4", - "LG 그램5", - "LG 그램6", - "LG 그램7" - ] + var recentSearchTexts: [String] = [] var isAutoSaveSearch: Bool = false var isSearching: Bool = false var isFiltered: Bool = false @@ -46,6 +41,10 @@ public struct PokitSearchFeature { var isResultAscending = true fileprivate var domain = Search() + var searchText: String { + get { domain.condition.searchWord } + set { domain.condition.searchWord = newValue } + } var resultMock: IdentifiedArrayOf? { guard let contentList = domain.contentList.data else { return nil @@ -59,8 +58,8 @@ public struct PokitSearchFeature { set { domain.condition.favorites = newValue } } var unreadFilter: Bool { - get { !domain.condition.isRead } - set { domain.condition.isRead = !newValue } + get { domain.condition.isRead } + set { domain.condition.isRead = newValue } } var startDateFilter: Date? { get { domain.condition.startDate } @@ -70,6 +69,15 @@ public struct PokitSearchFeature { get { domain.condition.endDate } set { domain.condition.endDate = newValue } } + var startDateString: String? { + guard let startDate = domain.condition.startDate else { + return nil + } + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + + return formatter.string(from: startDate) + } /// sheet item var bottomSheetItem: BaseContentItem? = nil @@ -108,7 +116,7 @@ public struct PokitSearchFeature { delegate: PokitBottomSheet.Delegate, content: BaseContentItem ) - case deleteAlertConfirmTapped(content: BaseContentItem) + case deleteAlertConfirmTapped case sortTextLinkTapped case backButtonTapped /// - TextInput OnSubmitted @@ -127,9 +135,19 @@ public struct PokitSearchFeature { case dismissBottomSheet case updateIsFiltered case updateCategoryIds + case 컨텐츠_목록_갱신(BaseContentListInquiry) + case 최근검색어_불러오기 + case 자동저장_켜기_불러오기 + case 최근검색어_추가 + case 컨텐츠_삭제_반영(id: Int) } - public enum AsyncAction: Equatable { case doNothing } + public enum AsyncAction: Equatable { + case 컨텐츠_검색 + case 최근검색어_갱신 + case 자동저장_켜기_갱신 + case 컨텐츠_삭제(id: Int) + } public enum ScopeAction: Equatable { case filterBottomSheet(FilterBottomFeature.Action.DelegateAction) @@ -141,7 +159,7 @@ public struct PokitSearchFeature { public enum DelegateAction: Equatable { case linkCardTapped(content: BaseContentItem) - case bottomSheetEditCellButtonTapped(content: BaseContentItem) + case 링크수정(contentId: Int) case linkCopyDetected(URL?) } } @@ -177,7 +195,7 @@ public struct PokitSearchFeature { return .none } } - + public enum CancelID { case response } /// - Reducer body public var body: some ReducerOf { BindingReducer(action: \.view) @@ -203,29 +221,26 @@ private extension PokitSearchFeature { return .none case .autoSaveButtonTapped: state.isAutoSaveSearch.toggle() - return .none + return .send(.async(.자동저장_켜기_갱신)) case .searchTextInputOnSubmitted: return .run { send in - // - TODO: 검색 조회 - await send(.inner(.enableIsSearching)) + await send(.inner(.최근검색어_추가)) + await send(.async(.컨텐츠_검색)) } case .searchTextInputIconTapped: /// - 검색 중일 경우 `문자열 지우기 버튼 동작` if state.isSearching { - state.searchText = "" + state.domain.condition.searchWord = "" return .send(.inner(.disableIsSearching)) } else { return .run { send in - // - TODO: 검색 조회 - await send(.inner(.enableIsSearching)) + await send(.inner(.최근검색어_추가)) + await send(.async(.컨텐츠_검색)) } } case .searchTextChipButtonTapped(text: let text): state.searchText = text - return .run { send in - // - TODO: 검색 조회 - await send(.inner(.enableIsSearching)) - } + return .send(.async(.컨텐츠_검색)) case .filterButtonTapped: return .send(.inner(.showFilterBottomSheet(filterType: .pokit))) case .contentTypeFilterButtonTapped: @@ -237,18 +252,21 @@ private extension PokitSearchFeature { } state.domain.condition.startDate = nil state.domain.condition.endDate = nil - return .send(.inner(.updateDateFilter(startDate: nil, endDate: nil))) + return .run { send in + await send(.inner(.updateDateFilter(startDate: nil, endDate: nil))) + await send(.async(.컨텐츠_검색)) + } case .categoryFilterButtonTapped: return .send(.inner(.showFilterBottomSheet(filterType: .pokit))) case .recentSearchAllRemoveButtonTapped: state.recentSearchTexts.removeAll() - return .none + return .send(.async(.최근검색어_갱신)) case .recentSearchChipIconTapped(searchText: let searchText): guard let predicate = state.recentSearchTexts.firstIndex(of: searchText) else { return .none } state.recentSearchTexts.remove(at: predicate) - return .none + return .send(.async(.최근검색어_갱신)) case .linkCardTapped(content: let content): return .send(.delegate(.linkCardTapped(content: content))) case .kebabButtonTapped(content: let content): @@ -259,9 +277,12 @@ private extension PokitSearchFeature { await send(.inner(.dismissBottomSheet)) await send(.scope(.bottomSheet(delegate: delegate, content: content))) } - case .deleteAlertConfirmTapped(content: let content): + case .deleteAlertConfirmTapped: + guard let id = state.alertItem?.id else { return .none } state.alertItem = nil - return .none + return .run { [id] send in + await send(.async(.컨텐츠_삭제(id: id))) + } case .sortTextLinkTapped: state.isResultAscending.toggle() // - TODO: 정렬 @@ -273,6 +294,8 @@ private extension PokitSearchFeature { case .onAppear: return .run { send in + await send(.inner(.자동저장_켜기_불러오기)) + await send(.inner(.최근검색어_불러오기)) for await _ in self.pasteboard.changes() { let url = try await pasteboard.probableWebURL() await send(.delegate(.linkCopyDetected(url)), animation: .pokitSpring) @@ -280,13 +303,16 @@ private extension PokitSearchFeature { } case .categoryFilterChipTapped(category: let category): state.categoryFilter.remove(category) - return .send(.inner(.updateCategoryIds)) + return .run { send in + await send(.inner(.updateCategoryIds)) + await send(.async(.컨텐츠_검색)) + } case .favoriteChipTapped: state.domain.condition.favorites = false - return .none + return .send(.async(.컨텐츠_검색)) case .unreadChipTapped: - state.domain.condition.isRead = true - return .none + state.domain.condition.isRead = false + return .send(.async(.컨텐츠_검색)) } } @@ -295,11 +321,10 @@ private extension PokitSearchFeature { switch action { case .enableIsSearching: state.isSearching = true - // - MARK: 더미 조회 - state.domain.contentList = ContentListInquiryResponse.mock.toDomain() return .none case .disableIsSearching: state.isSearching = false + state.domain.contentList.data = [] return .none case .updateDateFilter(startDate: let startDate, endDate: let endDate): let formatter = DateFormatter() @@ -334,7 +359,7 @@ private extension PokitSearchFeature { return .none case .updateContentTypeFilter(favoriteFilter: let favoriteFilter, unreadFilter: let unreadFilter): state.domain.condition.favorites = favoriteFilter - state.domain.condition.isRead = !unreadFilter + state.domain.condition.isRead = unreadFilter return .none case .dismissBottomSheet: state.bottomSheetItem = nil @@ -349,12 +374,92 @@ private extension PokitSearchFeature { case .updateCategoryIds: state.domain.condition.categoryIds = state.categoryFilter.map { $0.id } return .none + case .컨텐츠_목록_갱신(let contentList): + state.domain.contentList = contentList + return .send(.inner(.enableIsSearching)) + case .최근검색어_불러오기: + guard state.isAutoSaveSearch else { + return .none + } + state.recentSearchTexts = userDefaults.stringArrayKey(.searchWords) ?? [] + return .none + case .자동저장_켜기_불러오기: + state.isAutoSaveSearch = userDefaults.boolKey(.autoSaveSearch) + return .none + + case .최근검색어_추가: + guard state.isAutoSaveSearch else { return .none } + guard !state.domain.condition.searchWord.isEmpty else { return .none } + if !state.recentSearchTexts.contains(state.domain.condition.searchWord) { + state.recentSearchTexts.append(state.domain.condition.searchWord) + } + return .send(.async(.최근검색어_갱신)) + case .컨텐츠_삭제_반영(id: let id): + state.alertItem = nil + state.domain.contentList.data?.removeAll { $0.id == id } + return .none } } /// - Async Effect func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect { - return .none + switch action { + case .컨텐츠_검색: + state.domain.contentList.data = nil + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + + var startDateString: String? = nil + var endDateString: String? = nil + if let startDate = state.domain.condition.startDate { + startDateString = formatter.string(from: startDate) + } + if let endDate = state.domain.condition.endDate { + endDateString = formatter.string(from: endDate) + } + return .run { [ + pageable = state.domain.pageable, + condition = state.domain.condition, + startDateString, + endDateString + ] send in + let contentList = try await contentClient.컨텐츠_검색( + .init( + page: pageable.page, + size: pageable.size, + sort: pageable.sort + ), + .init( + searchWord: condition.searchWord, + categoryIds: condition.categoryIds, + isRead: condition.isRead, + favorites: condition.favorites, + startDate: startDateString, + endDate: endDateString + ) + ).toDomain() + await send(.inner(.컨텐츠_목록_갱신(contentList)), animation: .smooth) + } + case .최근검색어_갱신: + guard state.isAutoSaveSearch else { return .none } + return .run { [ searchWords = state.recentSearchTexts ] _ in + await userDefaults.setStringArray( + searchWords, + .searchWords + ) + } + case .자동저장_켜기_갱신: + return .run { [ + isAutoSaveSearch = state.isAutoSaveSearch + ] send in + await userDefaults.setBool(isAutoSaveSearch, .autoSaveSearch) + } + case .컨텐츠_삭제(id: let id): + return .run { [id] send in + let _ = try await contentClient.컨텐츠_삭제("\(id)") + await send(.inner(.컨텐츠_삭제_반영(id: id)), animation: .pokitSpring) + } + } } /// - Scope Effect @@ -372,7 +477,7 @@ private extension PokitSearchFeature { await send(.inner(.updateContentTypeFilter(favoriteFilter: isFavorite, unreadFilter: isUnread))) await send(.inner(.updateDateFilter(startDate: startDate, endDate: endDate))) await send(.inner(.updateIsFiltered)) - // - TODO: 검색 조회 + await send(.async(.컨텐츠_검색)) } case .bottomSheet(let delegate, let content): switch delegate { @@ -380,7 +485,7 @@ private extension PokitSearchFeature { state.alertItem = content return .none case .editCellButtonTapped: - return .send(.delegate(.bottomSheetEditCellButtonTapped(content: content))) + return .send(.delegate(.링크수정(contentId: content.id))) case .favoriteCellButtonTapped: return .none case .shareCellButtonTapped: diff --git a/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift b/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift index c5968e59..f6cd1caf 100644 --- a/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift +++ b/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift @@ -61,7 +61,7 @@ public extension PokitSearchView { "링크를 정말 삭제하시겠습니까?", message: "함께 저장한 모든 정보가 삭제되며, \n복구하실 수 없습니다.", confirmText: "삭제" - ) { send(.deleteAlertConfirmTapped(content: content)) } + ) { send(.deleteAlertConfirmTapped) } } .onAppear { send(.onAppear) } } diff --git a/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomFeature.swift b/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomFeature.swift index fae48d15..a15cbce5 100644 --- a/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomFeature.swift +++ b/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomFeature.swift @@ -16,6 +16,8 @@ public struct FilterBottomFeature { /// - Dependency @Dependency(\.dismiss) private var dismiss + @Dependency(\.categoryClient) + private var categoryClient /// - State @ObservableState public struct State: Equatable { @@ -83,12 +85,16 @@ public struct FilterBottomFeature { case favoriteButtonTapped case unreadButtonTapped - case filterBottomSheetOnAppeard + case pokitListOnAppeared } - public enum InnerAction: Equatable { case doNothing } + public enum InnerAction: Equatable { + case 카테고리_목록_갱신(categoryList: BaseCategoryListInquiry) + } - public enum AsyncAction: Equatable { case doNothing } + public enum AsyncAction: Equatable { + case 카테고리_목록_조회 + } public enum ScopeAction: Equatable { case doNothing } @@ -191,21 +197,36 @@ private extension FilterBottomFeature { case .unreadButtonTapped: state.isUnread.toggle() return .none - case .filterBottomSheetOnAppeard: - // - MARK: 더미 조회 - state.domain.categoryList = CategoryListInquiryResponse.mock.toDomain() - return .none + case .pokitListOnAppeared: + return .send(.async(.카테고리_목록_조회)) } } /// - Inner Effect func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect { - return .none + switch action { + case .카테고리_목록_갱신(categoryList: let categoryList): + state.domain.categoryList = categoryList + return .none + } } /// - Async Effect func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect { - return .none + switch action { + case .카테고리_목록_조회: + return .run { [pageable = state.domain.pageable] send in + let categoryList = try await categoryClient.카테고리_목록_조회( + .init( + page: pageable.page, + size: pageable.size, + sort: pageable.sort + ), + true + ).toDomain() + await send(.inner(.카테고리_목록_갱신(categoryList: categoryList)), animation: .smooth) + } + } } /// - Scope Effect diff --git a/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomSheet.swift b/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomSheet.swift index 7339fe7d..e6a80c19 100644 --- a/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomSheet.swift +++ b/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomSheet.swift @@ -33,15 +33,8 @@ public extension FilterBottomSheet { switch store.currentType { case .pokit: - if let pokitList = store.pokitList { - PokitList( - selectedItem: nil, - list: pokitList, - action: { send(.pokitListCellTapped(pokit: $0), animation: .pokitSpring) } - ) - } else { - PokitLoading() - } + pokitList + .onAppear { send(.pokitListOnAppeared) } case .contentType: contentTypes case .date: @@ -70,7 +63,6 @@ public extension FilterBottomSheet { .pokitPresentationCornerRadius() .presentationDragIndicator(.visible) .presentationDetents([.height(664)]) - .onAppear { send(.filterBottomSheetOnAppeard) } } } } @@ -101,6 +93,20 @@ private extension FilterBottomSheet { } } + var pokitList: some View { + Group { + if let pokitList = store.pokitList { + PokitList( + selectedItem: nil, + list: pokitList, + action: { send(.pokitListCellTapped(pokit: $0), animation: .pokitSpring) } + ) + } else { + PokitLoading() + } + } + } + var contentTypes: some View { VStack(spacing: 0) { contentTypeButton(