Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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())

Choose a reason for hiding this comment

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

medium

createdAt[Int] 타입으로 도메인 모델에 그대로 전달되고 있습니다. 도메인 모델에서는 Date와 같은 표준 타입을 사용하는 것이 좋습니다. 이 toDomain() 메서드 내에서 [Int]Date로 변환하는 로직을 추가하고, CollectionListResponsecreatedAt 타입도 Date로 변경하는 것을 권장합니다. 이렇게 하면 데이터 변환의 책임을 Data 레이어에 유지하고 도메인 모델을 더 견고하게 만들 수 있습니다.

}
}
13 changes: 13 additions & 0 deletions MLS/Data/Data/Network/Endpoints/CollectionEndPoint.swift
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)
}
}
35 changes: 35 additions & 0 deletions MLS/Data/Data/Repository/CollectionAPIRepositoryImpl.swift
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)

Choose a reason for hiding this comment

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

medium

불필요한 공백 라인이 있습니다. 코드 가독성을 위해 제거하는 것이 좋습니다.

}
}

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]>
}
12 changes: 11 additions & 1 deletion MLS/MLS/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ private extension AppDelegate {
DIContainer.register(type: AlarmAPIRepository.self) {
AlarmAPIRepositoryImpl(provider: DIContainer.resolve(type: NetworkProvider.self), interceptor: DIContainer.resolve(type: Interceptor.self))
}
DIContainer.register(type: CollectionAPIRepository.self) {
CollectionAPIRepositoryImpl(provider: DIContainer.resolve(type: NetworkProvider.self),
tokenInterceptor: DIContainer.resolve(type: Interceptor.self))
}
}

func registerUseCase() {
Expand Down Expand Up @@ -340,6 +344,12 @@ private extension AppDelegate {
DIContainer.register(type: ParseItemFilterResultUseCase.self) {
ParseItemFilterResultUseCaseImpl()
}
DIContainer.register(type: FetchCollectionListUseCase.self) {
FetchCollectionListUseCaseImpl(repository: DIContainer.resolve(type: CollectionAPIRepository.self))
}
DIContainer.register(type: CreateCollectionListUseCase.self) {
CreateCollectionListUseCaseImpl(repository: DIContainer.resolve(type: CollectionAPIRepository.self))
}
}

func registerFactory() {
Expand Down Expand Up @@ -478,7 +488,7 @@ private extension AppDelegate {
BookmarkListFactoryImpl(itemFilterFactory: DIContainer.resolve(type: ItemFilterBottomSheetFactory.self), monsterFilterFactory: DIContainer.resolve(type: MonsterFilterBottomSheetFactory.self), sortedFactory: DIContainer.resolve(type: SortedBottomSheetFactory.self), bookmarkModalFactory: DIContainer.resolve(type: BookmarkModalFactory.self), loginFactory: DIContainer.resolve(type: LoginFactory.self), dictionaryDetailFactory: DIContainer.resolve(type: DictionaryDetailFactory.self), setBookmarkUseCase: DIContainer.resolve(type: SetBookmarkUseCase.self), checkLoginUseCase: DIContainer.resolve(type: CheckLoginUseCase.self), fetchBookmarkUseCase: DIContainer.resolve(type: FetchBookmarkUseCase.self), fetchMonsterBookmarkUseCase: DIContainer.resolve(type: FetchMonsterBookmarkUseCase.self), fetchItemBookmarkUseCase: DIContainer.resolve(type: FetchItemBookmarkUseCase.self), fetchNPCBookmarkUseCase: DIContainer.resolve(type: FetchNPCBookmarkUseCase.self), fetchQuestBookmarkUseCase: DIContainer.resolve(type: FetchQuestBookmarkUseCase.self), fetchMapBookmarkUseCase: DIContainer.resolve(type: FetchMapBookmarkUseCase.self), collectionEditFactory: DIContainer.resolve(type: CollectionEditFactory.self))
}
DIContainer.register(type: CollectionListFactory.self) {
CollectionListFactoryImpl(addCollectionFactory: DIContainer.resolve(type: AddCollectionFactory.self), bookmarkDetailFactory: DIContainer.resolve(type: CollectionDetailFactory.self))
CollectionListFactoryImpl(collectionListUseCase: DIContainer.resolve(type: FetchCollectionListUseCase.self), createCollectionListUseCase: DIContainer.resolve(type: CreateCollectionListUseCase.self), addCollectionFactory: DIContainer.resolve(type: AddCollectionFactory.self), bookmarkDetailFactory: DIContainer.resolve(type: CollectionDetailFactory.self))
}
DIContainer.register(type: CollectionDetailFactory.self) {
CollectionDetailFactoryImpl(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@ public final class AddCollectionModalReactor: Reactor {
newState.route = route
case .addCollection(let title):
var collection = newState.collection
collection?.title = title
// 기존 collection이 없으면 새로 생성
if collection == nil {
collection = BookmarkCollection(id: -1, title: title, items: [])
} else {
collection?.title = title
}
newState.route = .dismissWithSuccess(collection)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import BaseFeature
import UIKit

import DesignSystem
Expand Down Expand Up @@ -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

Choose a reason for hiding this comment

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

medium

디버깅 목적으로 사용된 print 구문이 코드에 남아있습니다. 프로덕션 코드에 포함되지 않도록 제거해주세요.

        loadImages(from: input.images) { [weak self] images in
            self?.cellView.setImages(images: images)
        }

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

Choose a reason for hiding this comment

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

medium

loadImages 함수가 파일 최상단에 private으로 선언되어 있습니다. 이 함수는 CollectionListCell에서만 사용되므로, CollectionListCellprivate 메서드로 옮겨서 클래스와의 연관성을 명확히 하고 코드의 응집도를 높이는 것이 좋습니다.

Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import BaseFeature
import BookmarkFeatureInterface
import DomainInterface

public final class CollectionListFactoryImpl: CollectionListFactory {
private let collectionListUseCase: FetchCollectionListUseCase
private let createCollectionListUseCase: CreateCollectionListUseCase
private let addCollectionFactory: AddCollectionFactory
private let bookmarkDetailFactory: CollectionDetailFactory

public init(addCollectionFactory: AddCollectionFactory, bookmarkDetailFactory: CollectionDetailFactory) {
public init(collectionListUseCase: FetchCollectionListUseCase, createCollectionListUseCase: CreateCollectionListUseCase, addCollectionFactory: AddCollectionFactory, bookmarkDetailFactory: CollectionDetailFactory) {
self.collectionListUseCase = collectionListUseCase
self.createCollectionListUseCase = createCollectionListUseCase
self.addCollectionFactory = addCollectionFactory
self.bookmarkDetailFactory = bookmarkDetailFactory
}

public func make() -> BaseViewController {
let reactor = CollectionListReactor()
let reactor = CollectionListReactor(collectionListUseCase: collectionListUseCase, createCollectionListUseCase: createCollectionListUseCase)
let viewController = CollectionListViewController(addCollectionFactory: addCollectionFactory, detailFactory: bookmarkDetailFactory)
viewController.reactor = reactor
return viewController
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Choose a reason for hiding this comment

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

high

Statecollections 속성은 API 연동으로 인해 더 이상 사용되지 않는 것으로 보입니다. itemTapped 액션에서 잘못 사용되고 있어 버그를 유발하고 있습니다. collectionListData를 사용하도록 수정한 뒤, 이 속성을 제거하여 혼동을 줄이고 코드를 정리하는 것이 좋습니다.

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

Choose a reason for hiding this comment

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

high

init에서 collections 속성에 더미 데이터를 할당하는 코드는 더 이상 필요하지 않습니다. collections 속성이 제거되면 이 코드도 함께 제거하여 initialState를 간소화할 수 있습니다.

        self.initialState = State(route: .none, collectionListData: [])

}

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])))

Choose a reason for hiding this comment

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

critical

itemTapped 액션이 currentState.collections를 사용하여 상세 페이지로 이동하고 있습니다. 하지만 API 연동 후 실제 데이터는 collectionListData에 저장되므로, collections는 더미 데이터를 담고 있거나 collectionListData와 동기화되지 않은 상태일 수 있습니다. 이로 인해 잘못된 데이터로 상세 페이지가 열리거나, 인덱스가 맞지 않아 앱이 크래시될 수 있습니다. collectionListData의 데이터를 사용하도록 수정해야 합니다.

        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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ private extension CollectionListViewController {
addFloatingButton { [weak self] in
guard let self = self else { return }
let viewController = self.addCollectionFactory.make(collection: nil, onDismissWithMessage: { [weak self] collection in
if let collection = collection {
// Reactor에게 새로운 콜렉션 추가를 알림
self?.reactor?.action.onNext(.addCollection(collection.title))
}
self?.onDismissWithMessage?(collection)
})
self.present(viewController, animated: true)
Expand All @@ -83,7 +87,12 @@ extension CollectionListViewController {
bindViewState(reactor: reactor)
}

func bindUserActions(reactor: Reactor) {}
func bindUserActions(reactor: Reactor) {
rx.viewWillAppear
.map { Reactor.Action.viewWillAppear }
.bind(to: reactor.action)
.disposed(by: disposeBag)
}

func bindViewState(reactor: Reactor) {
rx.viewDidAppear
Expand All @@ -102,10 +111,11 @@ extension CollectionListViewController {
.disposed(by: disposeBag)

reactor.state
.map(\.collections)
.map(\.collectionListData)
.withUnretained(self)
.subscribe { owner, collections in
owner.mainView.updateView(isEmptyData: collections.isEmpty)
.observe(on: MainScheduler.instance)
.subscribe { owner, collectionListData in
owner.mainView.updateView(isEmptyData: collectionListData.isEmpty)
owner.mainView.listCollectionView.reloadData()
}
.disposed(by: disposeBag)
Expand All @@ -115,7 +125,7 @@ extension CollectionListViewController {
// MARK: - Delegate
extension CollectionListViewController: UICollectionViewDelegate, UICollectionViewDataSource {
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
reactor?.currentState.collections.count ?? 0
reactor?.currentState.collectionListData.count ?? 0
}

public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
Expand All @@ -124,11 +134,11 @@ extension CollectionListViewController: UICollectionViewDelegate, UICollectionVi
withReuseIdentifier: CollectionListCell.identifier,
for: indexPath
) as? CollectionListCell,
let item = reactor?.currentState.collections[indexPath.row]
let item = reactor?.currentState.collectionListData[indexPath.row]
else {
return UICollectionViewCell()
}
cell.inject(input: CollectionListCell.Input(title: item.title, count: item.count, images: item.thumbnails))
cell.inject(input: CollectionListCell.Input(title: item.name, count: item.recentBookmarks.count, images: item.recentBookmarks.map { $0.imageUrl }))
return cell
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Choose a reason for hiding this comment

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

medium

디버깅 목적으로 사용된 print 구문이 코드에 남아있습니다. 프로덕션 코드에 포함되지 않도록 제거해주세요.

        for (index, view) in imageViews.enumerated() {
            let imageView = view.subviews.compactMap { $0 as? UIImageView }.first
            imageView?.image = index < images.count ? images[index] : nil
        }

Expand Down
Loading