diff --git a/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListFeature.swift b/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListFeature.swift index 0e4404a0..b5797db0 100644 --- a/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListFeature.swift +++ b/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListFeature.swift @@ -66,40 +66,40 @@ public struct ContentListFeature { public enum View: Equatable, BindableAction { /// - Binding case binding(BindingAction) + + case pagenation /// - Button Tapped - case linkCardTapped(content: BaseContentItem) - case kebabButtonTapped(content: BaseContentItem) - case bottomSheetButtonTapped( + case bottomSheet( delegate: PokitBottomSheet.Delegate, content: BaseContentItem ) - case deleteAlertConfirmTapped(content: BaseContentItem) - case sortTextLinkTapped - case backButtonTapped + case 컨텐츠_항목_눌렀을때(content: BaseContentItem) + case 컨텐츠_항목_케밥_버튼_눌렀을때(content: BaseContentItem) + case 컨텐츠_삭제_눌렀을때(content: BaseContentItem) + case 정렬_버튼_눌렀을때 + case dismiss /// - On Appeared - case contentListViewOnAppeared - case pagenation + case 뷰가_나타났을때 - case 링크_공유_완료 + case 링크_공유시트_해제 + case 경고시트_해제 } public enum InnerAction: Equatable { - case dismissBottomSheet - case 컨텐츠_목록_조회(BaseContentListInquiry) - case 컨텐츠_삭제_반영(id: Int) - case pagenation_네트워크_결과(BaseContentListInquiry) - case pagenation_초기화 - case 컨텐츠_목록_갱신(BaseContentListInquiry) - case 컨텐츠_개수_갱신(Int) + case 바텀시트_해제 + case 컨텐츠_목록_조회_API_반영(BaseContentListInquiry) + case 컨텐츠_삭제_API_반영(id: Int) + case 컨텐츠_목록_조회_페이징_API_반영(BaseContentListInquiry) + case 페이징_초기화 + case 컨텐츠_개수_업데이트(Int) } public enum AsyncAction: Equatable { - case 읽지않음_컨텐츠_조회 - case 즐겨찾기_링크모음_조회 - case 컨텐츠_삭제(id: Int) - case pagenation_네트워크 - case 페이징_재조회 - case 컨텐츠_개수_조회 + case 컨텐츠_삭제_API(id: Int) + case 컨텐츠_목록_조회_페이징_API + case 컨텐츠_목록_조회_API + case 컨텐츠_개수_조회_API + case 클립보드_감지 } public enum ScopeAction: Equatable { @@ -148,6 +148,7 @@ public struct ContentListFeature { /// - Reducer body public var body: some ReducerOf { Reduce(self.core) + ._printChanges() } } //MARK: - FeatureAction Effect @@ -155,87 +156,71 @@ private extension ContentListFeature { /// - View Effect func handleViewAction(_ action: Action.View, state: inout State) -> Effect { switch action { - case .kebabButtonTapped(let content): + case .컨텐츠_항목_케밥_버튼_눌렀을때(let content): state.bottomSheetItem = content return .none - case .linkCardTapped(let content): + case .컨텐츠_항목_눌렀을때(let content): return .send(.delegate(.링크상세(content: content))) - case .bottomSheetButtonTapped(let delegate, let content): - return .run { send in - await send(.inner(.dismissBottomSheet)) - await send(.scope(.bottomSheet(delegate: delegate, content: content))) - } - case .deleteAlertConfirmTapped: + case .bottomSheet(let delegate, let content): + return .concatenate( + .send(.inner(.바텀시트_해제)), + .send(.scope(.bottomSheet(delegate: delegate, content: content))) + ) + case .컨텐츠_삭제_눌렀을때: guard let id = state.alertItem?.id else { return .none } - return .run { [id] send in - await send(.async(.컨텐츠_삭제(id: id))) - } + return .send(.async(.컨텐츠_삭제_API(id: id))) case .binding: return .none - case .sortTextLinkTapped: + case .정렬_버튼_눌렀을때: state.isListDescending.toggle() state.domain.pageable.sort = [ state.isListDescending ? "createdAt,desc" : "createdAt,asc" ] - return .send(.inner(.pagenation_초기화), animation: .pokitDissolve) - case .backButtonTapped: + return .send(.inner(.페이징_초기화), animation: .pokitDissolve) + case .dismiss: return .run { _ in await dismiss() } - case .contentListViewOnAppeared: - return .run { [type = state.contentType] send in - switch type { - case .unread: - await send(.async(.읽지않음_컨텐츠_조회), animation: .pokitDissolve) - case .favorite: - await send(.async(.즐겨찾기_링크모음_조회), animation: .pokitDissolve) - } - - for await _ in self.pasteBoard.changes() { - let url = try await pasteBoard.probableWebURL() - await send(.delegate(.linkCopyDetected(url)), animation: .pokitSpring) - } - } - + case .뷰가_나타났을때: + return .merge( + .send(.async(.컨텐츠_개수_조회_API)), + .send(.async(.컨텐츠_목록_조회_API)), + .send(.async(.클립보드_감지)) + ) case .pagenation: - return .send(.async(.pagenation_네트워크)) - case .링크_공유_완료: + return .send(.async(.컨텐츠_목록_조회_페이징_API)) + case .링크_공유시트_해제: state.shareSheetItem = nil return .none + case .경고시트_해제: + state.alertItem = nil + return .none } } /// - Inner Effect func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect { switch action { - case .dismissBottomSheet: + case .바텀시트_해제: state.bottomSheetItem = nil return .none - case .컨텐츠_목록_조회(let contentList): + case .컨텐츠_목록_조회_페이징_API_반영(let contentList): let list = state.domain.contentList.data ?? [] guard let newList = contentList.data else { return .none } state.domain.contentList = contentList state.domain.contentList.data = list + newList return .none - case .컨텐츠_삭제_반영(id: let id): + case .컨텐츠_삭제_API_반영(id: let id): state.alertItem = nil state.domain.contentList.data?.removeAll { $0.id == id } return .none - case .pagenation_네트워크_결과(let contentList): + case .컨텐츠_목록_조회_API_반영(let contentList): state.domain.contentList = contentList return .none - case .pagenation_초기화: + case .페이징_초기화: state.domain.pageable.page = 0 state.domain.contentList.data = nil - switch state.contentType { - case .unread: - return .send(.async(.읽지않음_컨텐츠_조회), animation: .pokitDissolve) - case .favorite: - return .send(.async(.즐겨찾기_링크모음_조회), animation: .pokitDissolve) - } - case let .컨텐츠_목록_갱신(contentList): - state.domain.contentList = contentList - return .none - case let .컨텐츠_개수_갱신(count): + return .send(.async(.컨텐츠_목록_조회_API), animation: .pokitDissolve) + case let .컨텐츠_개수_업데이트(count): state.domain.contentCount = count return .none } @@ -244,105 +229,58 @@ private extension ContentListFeature { /// - Async Effect func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect { switch action { - case .읽지않음_컨텐츠_조회: - return .run { [pageable = state.domain.pageable] send in - await send(.async(.컨텐츠_개수_조회)) - let contentList = try await remindClient.읽지않음_컨텐츠_조회( - BasePageableRequest( - page: pageable.page, - size: pageable.size, - sort: pageable.sort - ) - ).toDomain() - await send( - .inner(.컨텐츠_목록_조회(contentList)), - animation: pageable.page == 0 ? .pokitDissolve : nil - ) - } - case .즐겨찾기_링크모음_조회: - return .run { [pageable = state.domain.pageable] send in - await send(.async(.컨텐츠_개수_조회)) - let contentList = try await remindClient.즐겨찾기_링크모음_조회( - BasePageableRequest( - page: pageable.page, - size: pageable.size, - sort: pageable.sort - ) - ).toDomain() - await send( - .inner(.컨텐츠_목록_조회(contentList)), - animation: pageable.page == 0 ? .pokitDissolve : nil - ) - } - case .컨텐츠_삭제(id: let id): - return .run { [count = state.domain.contentCount] send in - let newCount = count - 1 - await send(.inner(.컨텐츠_개수_갱신(newCount)), animation: .pokitSpring) - let _ = try await contentClient.컨텐츠_삭제("\(id)") - await send(.inner(.컨텐츠_삭제_반영(id: id)), animation: .pokitSpring) - } + case .컨텐츠_삭제_API(id: let id): + let count = state.domain.contentCount + let newCount = count - 1 + + return .merge( + .send(.inner(.컨텐츠_개수_업데이트(newCount))), + contentDelete(contentId: id) + ) - case .pagenation_네트워크: + case .컨텐츠_목록_조회_페이징_API: state.domain.pageable.page += 1 - return .run { [type = state.contentType] send in + return .run { [ + type = state.contentType, + pageableRequest = BasePageableRequest( + page: state.domain.pageable.page, + size: state.domain.pageable.size, + sort: state.domain.pageable.sort + ) + ] send in + let contentList: BaseContentListInquiry switch type { case .unread: - await send(.async(.읽지않음_컨텐츠_조회)) - break + contentList = try await remindClient.읽지않음_컨텐츠_조회( + pageableRequest + ).toDomain() case .favorite: - await send(.async(.즐겨찾기_링크모음_조회)) - break + contentList = try await remindClient.즐겨찾기_링크모음_조회( + pageableRequest + ).toDomain() } + + await send(.inner(.컨텐츠_목록_조회_페이징_API_반영(contentList))) } - case .페이징_재조회: - return .run { [ - pageable = state.domain.pageable, - contentType = state.contentType - ] send in - await send(.async(.컨텐츠_개수_조회)) - let stream = AsyncThrowingStream { continuation in - Task { - for page in 0...pageable.page { - let paeagableRequest = BasePageableRequest( - page: page, - size: pageable.size, - sort: pageable.sort - ) - switch contentType { - case .favorite: - let contentList = try await remindClient.즐겨찾기_링크모음_조회( - paeagableRequest - ).toDomain() - continuation.yield(contentList) - case .unread: - let contentList = try await remindClient.읽지않음_컨텐츠_조회( - paeagableRequest - ).toDomain() - continuation.yield(contentList) - } - } - continuation.finish() - } - } - var contentItems: BaseContentListInquiry? = nil - for try await contentList in stream { - let items = contentItems?.data ?? [] - let newItems = contentList.data ?? [] - contentItems = contentList - contentItems?.data = items + newItems - } - guard let contentItems else { return } - await send(.inner(.컨텐츠_목록_갱신(contentItems)), animation: .pokitSpring) - } - case .컨텐츠_개수_조회: + case .컨텐츠_목록_조회_API: + return contentListFetch(state: &state) + case .컨텐츠_개수_조회_API: return .run { [ contentType = state.contentType ] send in + let count: Int switch contentType { case .favorite: - let count = try await remindClient.즐겨찾기_컨텐츠_개수_조회().count - await send(.inner(.컨텐츠_개수_갱신(count)), animation: .pokitSpring) + count = try await remindClient.즐겨찾기_컨텐츠_개수_조회().count case .unread: - let count = try await remindClient.읽지않음_컨텐츠_개수_조회().count - await send(.inner(.컨텐츠_개수_갱신(count)), animation: .pokitSpring) + count = try await remindClient.읽지않음_컨텐츠_개수_조회().count + } + + await send(.inner(.컨텐츠_개수_업데이트(count)), animation: .pokitSpring) + } + case .클립보드_감지: + return .run { send in + for await _ in self.pasteBoard.changes() { + let url = try await pasteBoard.probableWebURL() + await send(.delegate(.linkCopyDetected(url)), animation: .pokitSpring) } } } @@ -372,11 +310,59 @@ private extension ContentListFeature { func handleDelegateAction(_ action: Action.DelegateAction, state: inout State) -> Effect { switch action { case .컨텐츠_목록_조회: - return .send(.async(.페이징_재조회)) + return .send(.async(.컨텐츠_목록_조회_API)) default: return .none } } + + func contentListFetch(state: inout State) -> Effect { + return .run { [ + pageable = state.domain.pageable, + contentType = state.contentType + ] send in + let stream = AsyncThrowingStream { continuation in + Task { + for page in 0...pageable.page { + let paeagableRequest = BasePageableRequest( + page: page, + size: pageable.size, + sort: pageable.sort + ) + switch contentType { + case .favorite: + let contentList = try await remindClient.즐겨찾기_링크모음_조회( + paeagableRequest + ).toDomain() + continuation.yield(contentList) + case .unread: + let contentList = try await remindClient.읽지않음_컨텐츠_조회( + paeagableRequest + ).toDomain() + continuation.yield(contentList) + } + } + continuation.finish() + } + } + var contentItems: BaseContentListInquiry? = nil + for try await contentList in stream { + let items = contentItems?.data ?? [] + let newItems = contentList.data ?? [] + contentItems = contentList + contentItems?.data = items + newItems + } + guard let contentItems else { return } + await send(.inner(.컨텐츠_목록_조회_API_반영(contentItems))) + } + } + + func contentDelete(contentId: Int) -> Effect { + return .run { send in + let _ = try await contentClient.컨텐츠_삭제("\(contentId)") + await send(.inner(.컨텐츠_삭제_API_반영(id: contentId)), animation: .pokitSpring) + } + } } public extension ContentListFeature { diff --git a/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListView.swift b/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListView.swift index 1fb27e51..53b6de19 100644 --- a/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListView.swift +++ b/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListView.swift @@ -38,7 +38,7 @@ public extension ContentListView { items: [.share, .edit, .delete], height: 224, delegateSend: { - send(.bottomSheetButtonTapped(delegate: $0, content: content)) + send(.bottomSheet(delegate: $0, content: content)) } ) } @@ -46,7 +46,7 @@ public extension ContentListView { if let shareURL = URL(string: content.data) { PokitShareSheet( items: [shareURL], - completion: { send(.링크_공유_완료) } + completion: { send(.링크_공유시트_해제) } ) .presentationDetents([.medium, .large]) } @@ -55,10 +55,12 @@ public extension ContentListView { PokitAlert( "링크를 정말 삭제하시겠습니까?", message: "함께 저장한 모든 정보가 삭제되며, \n복구하실 수 없습니다.", - confirmText: "삭제" - ) { send(.deleteAlertConfirmTapped(content: content)) } + confirmText: "삭제", + action: { send(.컨텐츠_삭제_눌렀을때(content: content)) }, + cancelAction: { send(.경고시트_해제) } + ) } - .task { await send(.contentListViewOnAppeared, animation: .pokitDissolve).finish() } + .task { await send(.뷰가_나타났을때, animation: .pokitDissolve).finish() } } } } @@ -76,7 +78,7 @@ private extension ContentListView { PokitIconLTextLink( store.isListDescending ? "최신순" : "오래된순", icon: .icon(.align), - action: { send(.sortTextLinkTapped) } + action: { send(.정렬_버튼_눌렀을때) } ) .contentTransition(.numericText()) } @@ -103,8 +105,8 @@ private extension ContentListView { PokitLinkCard( link: content, - action: { send(.linkCardTapped(content: content)) }, - kebabAction: { send(.kebabButtonTapped(content: content)) } + action: { send(.컨텐츠_항목_눌렀을때(content: content)) }, + kebabAction: { send(.컨텐츠_항목_케밥_버튼_눌렀을때(content: content)) } ) .divider(isFirst: isFirst, isLast: isLast) } @@ -128,7 +130,7 @@ private extension ContentListView { PokitHeader(title: store.contentType.title) { PokitHeaderItems(placement: .leading) { PokitToolbarButton(.icon(.arrowLeft)) { - send(.backButtonTapped) + send(.dismiss) } } }