-
Notifications
You must be signed in to change notification settings - Fork 0
컬렉션 API 연동 #270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
컬렉션 API 연동 #270
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import DomainInterface | ||
|
|
||
| public struct CollectionListResponseDTO: Decodable { | ||
| public let collectionId: Int | ||
| public let name: String | ||
| public let createdAt: [Int] | ||
| public let recentBookmarks: [BookmarkDTO] | ||
|
|
||
| public func toDomain() -> CollectionListResponse { | ||
| return CollectionListResponse(collectionId: collectionId, name: name, createdAt: createdAt, recentBookmarks: recentBookmarks.toDomain()) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import DomainInterface | ||
|
|
||
| public enum CollectionEndPoint { | ||
| static let base = "https://api.mapleland.kro.kr" | ||
|
|
||
| public static func fetchCollectionList() -> ResponsableEndPoint<[CollectionListResponseDTO]> { | ||
| .init(baseURL: base, path: "/api/v1/collections", method: .GET) | ||
| } | ||
|
|
||
| public static func createCollectionList(body: Encodable) -> EndPoint { | ||
| .init(baseURL: base, path: "/api/v1/collections", method: .POST, body: body) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import Foundation | ||
|
|
||
| import DomainInterface | ||
|
|
||
| import RxSwift | ||
|
|
||
| public class CollectionAPIRepositoryImpl: CollectionAPIRepository { | ||
| private let provider: NetworkProvider | ||
| private let tokenInterceptor: Interceptor | ||
|
|
||
| public init(provider: NetworkProvider, tokenInterceptor: Interceptor) { | ||
| self.provider = provider | ||
| self.tokenInterceptor = tokenInterceptor | ||
| } | ||
|
|
||
| public func fetchCollectionList() -> Observable<[CollectionListResponse]> { | ||
| let endPoint = CollectionEndPoint.fetchCollectionList() | ||
| return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor).map { | ||
| $0.map {$0.toDomain()} | ||
| } | ||
| } | ||
|
|
||
| public func createCollectionList(name: String) -> Completable { | ||
| let endPoint = CollectionEndPoint.createCollectionList(body: CreateCollectionRequestDTO(name: name)) | ||
| return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor) | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| } | ||
|
|
||
| private extension CollectionAPIRepositoryImpl { | ||
| struct CreateCollectionRequestDTO: Encodable { | ||
| let name: String | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import DomainInterface | ||
|
|
||
| import RxSwift | ||
|
|
||
| public final class CreateCollectionListUseCaseImpl: CreateCollectionListUseCase { | ||
| private let repository: CollectionAPIRepository | ||
|
|
||
| public init(repository: CollectionAPIRepository) { | ||
| self.repository = repository | ||
| } | ||
|
|
||
| public func execute(name: String) -> Completable { | ||
| return repository.createCollectionList(name: name) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import DomainInterface | ||
|
|
||
| import RxSwift | ||
|
|
||
| public final class FetchCollectionListUseCaseImpl: FetchCollectionListUseCase { | ||
| private let repository: CollectionAPIRepository | ||
|
|
||
| public init(repository: CollectionAPIRepository) { | ||
| self.repository = repository | ||
| } | ||
|
|
||
| public func execute() -> Observable<[CollectionListResponse]> { | ||
| return repository.fetchCollectionList() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| public struct CollectionListResponse { | ||
| public let collectionId: Int | ||
| public let name: String | ||
| public let createdAt: [Int] | ||
| public let recentBookmarks: [BookmarkResponse] | ||
|
|
||
| public init(collectionId: Int, name: String, createdAt: [Int], recentBookmarks: [BookmarkResponse]) { | ||
| self.collectionId = collectionId | ||
| self.name = name | ||
| self.createdAt = createdAt | ||
| self.recentBookmarks = recentBookmarks | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import Foundation | ||
|
|
||
| import RxSwift | ||
|
|
||
| public protocol CollectionAPIRepository { | ||
| // 컬렉션 목록 조회 | ||
| func fetchCollectionList() -> Observable<[CollectionListResponse]> | ||
| // 컬렉션 목록 추가 | ||
| func createCollectionList(name: String) -> Completable | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import RxSwift | ||
|
|
||
| public protocol CreateCollectionListUseCase { | ||
| func execute(name: String) -> Completable | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import RxSwift | ||
|
|
||
| public protocol FetchCollectionListUseCase { | ||
| func execute() -> Observable<[CollectionListResponse]> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import BaseFeature | ||
| import UIKit | ||
|
|
||
| import DesignSystem | ||
|
|
@@ -39,18 +40,40 @@ public extension CollectionListCell { | |
| struct Input { | ||
| let title: String | ||
| let count: Int | ||
| let images: [UIImage?] | ||
| let images: [String?] | ||
|
|
||
| public init(title: String, count: Int, images: [UIImage?]) { | ||
| public init(title: String, count: Int, images: [String?]) { | ||
| self.title = title | ||
| self.count = count | ||
| self.images = images | ||
| } | ||
| } | ||
|
|
||
| func inject(input: Input) { | ||
| cellView.setImages(images: input.images) | ||
| loadImages(from: input.images) { [weak self] images in | ||
| print("이미지:\(images)") | ||
| self?.cellView.setImages(images: images) | ||
| } | ||
|
Comment on lines
+53
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| cellView.setTitle(text: input.title) | ||
| cellView.setSubtitle(text: "\(String(input.count))개") | ||
| } | ||
| } | ||
|
|
||
| private func loadImages(from urls: [String?], completion: @escaping ([UIImage?]) -> Void) { | ||
|
|
||
| var results = [UIImage?](repeating: nil, count: urls.count) | ||
| let dispatchGroup = DispatchGroup() | ||
|
|
||
| for (index, urlString) in urls.enumerated() { | ||
| dispatchGroup.enter() | ||
|
|
||
| ImageLoader.shared.loadImage(stringURL: urlString) { image in | ||
| results[index] = image | ||
| dispatchGroup.leave() | ||
| } | ||
| } | ||
|
|
||
| dispatchGroup.notify(queue: .main) { | ||
| completion(results) | ||
| } | ||
| } | ||
|
Comment on lines
+62
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,45 +13,64 @@ public final class CollectionListReactor: Reactor { | |
|
|
||
| public enum Action { | ||
| case itemTapped(Int) | ||
| case viewWillAppear | ||
| case addCollection(String) | ||
| } | ||
|
|
||
| public enum Mutation { | ||
| case navigateTo(Route) | ||
| case setListData([CollectionListResponse]) | ||
| } | ||
|
|
||
| public struct State { | ||
| @Pulse var route: Route | ||
| var collections: [BookmarkCollection] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| var collectionListData: [CollectionListResponse] | ||
| } | ||
|
|
||
| // MARK: - Properties | ||
| public var initialState: State | ||
|
|
||
| private let disposeBag = DisposeBag() | ||
|
|
||
| public init() { | ||
| private let collectionListUseCase: FetchCollectionListUseCase | ||
| private let createCollectionListUseCase: CreateCollectionListUseCase | ||
|
|
||
| public init( | ||
| collectionListUseCase: FetchCollectionListUseCase, | ||
| createCollectionListUseCase: CreateCollectionListUseCase | ||
| ) { | ||
| self.collectionListUseCase = collectionListUseCase | ||
| self.createCollectionListUseCase = createCollectionListUseCase | ||
| self.initialState = State(route: .none, collections: [ | ||
| BookmarkCollection(id: 1, title: "1번", items: [ | ||
| BookmarkCollection(id: 1, title: "1000번", items: [ | ||
| DictionaryItem(id: 1, type: .item, mainText: "1번 아이템", subText: "1번 설명", image: .add, isBookmarked: false), | ||
| DictionaryItem(id: 2, type: .item, mainText: "2번 아이템", subText: "2번 설명", image: .add, isBookmarked: false) | ||
| ]), | ||
| BookmarkCollection(id: 2, title: "2번", items: [ | ||
| BookmarkCollection(id: 2, title: "2000번", items: [ | ||
| DictionaryItem(id: 3, type: .item, mainText: "3번 아이템", subText: "3번 설명", image: .add, isBookmarked: false), | ||
| DictionaryItem(id: 4, type: .item, mainText: "4번 아이템", subText: "4번 설명", image: .add, isBookmarked: false) | ||
| ]) | ||
| ]) | ||
| ], collectionListData: []) | ||
|
Comment on lines
45
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
|
||
| public func mutate(action: Action) -> Observable<Mutation> { | ||
| switch action { | ||
| case .viewWillAppear: | ||
| return collectionListUseCase.execute().map { .setListData($0) } | ||
| case .itemTapped(let index): | ||
| return .just(.navigateTo(.detail(currentState.collections[index]))) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
case .itemTapped(let index):
// FIXME: `collections`는 더미 데이터입니다. `collectionListData`를 사용해야 합니다.
// `collectionListData[index]`를 `BookmarkCollection`으로 변환하는 로직이 필요합니다.
return .just(.navigateTo(.detail(currentState.collections[index]))) |
||
| case .addCollection(let collection): | ||
| return createCollectionListUseCase.execute(name: collection).andThen(collectionListUseCase.execute()) | ||
| .map {.setListData($0)} | ||
| } | ||
| } | ||
|
|
||
| public func reduce(state: State, mutation: Mutation) -> State { | ||
| var newState = state | ||
| switch mutation { | ||
| case .setListData(let data): | ||
| newState.collectionListData = data | ||
| case .navigateTo(let route): | ||
| newState.route = route | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -136,6 +136,7 @@ public extension CollectionList { | |
| func setImages(images: [UIImage?]) { | ||
| for (index, view) in imageViews.enumerated() { | ||
| let imageView = view.subviews.compactMap { $0 as? UIImageView }.first | ||
| print("이미지 뷰 설정") | ||
| imageView?.image = index < images.count ? images[index] : nil | ||
| } | ||
| } | ||
|
Comment on lines
138
to
142
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
createdAt이[Int]타입으로 도메인 모델에 그대로 전달되고 있습니다. 도메인 모델에서는Date와 같은 표준 타입을 사용하는 것이 좋습니다. 이toDomain()메서드 내에서[Int]를Date로 변환하는 로직을 추가하고,CollectionListResponse의createdAt타입도Date로 변경하는 것을 권장합니다. 이렇게 하면 데이터 변환의 책임을 Data 레이어에 유지하고 도메인 모델을 더 견고하게 만들 수 있습니다.