diff --git a/FLINT/Data/Sources/DTO/Collection/ReportRequestDTO.swift b/FLINT/Data/Sources/DTO/Collection/ReportRequestDTO.swift new file mode 100644 index 0000000..e4a4130 --- /dev/null +++ b/FLINT/Data/Sources/DTO/Collection/ReportRequestDTO.swift @@ -0,0 +1,16 @@ +// +// ReportRequestDTO.swift +// Data +// + +import Foundation + +public struct ReportRequestDTO: Encodable { + public let reasons: [String] + public let otherDetail: String? + + public init(reasons: [String], otherDetail: String?) { + self.reasons = reasons + self.otherDetail = otherDetail + } +} diff --git a/FLINT/Data/Sources/Networking/API/CollectionAPI.swift b/FLINT/Data/Sources/Networking/API/CollectionAPI.swift index c5a50aa..0e07b57 100644 --- a/FLINT/Data/Sources/Networking/API/CollectionAPI.swift +++ b/FLINT/Data/Sources/Networking/API/CollectionAPI.swift @@ -10,6 +10,7 @@ import Foundation import Moya import Domain +import DTO public enum CollectionAPI { case fetchCollections(cursor: Int64?, size: Int32) @@ -18,6 +19,7 @@ public enum CollectionAPI { case deleteCollection(collectionId: Int64) case fetchCollectionDetail(collectionId: Int64) case fetchRecentViewedCollections + case reportCollection(collectionId: Int64, reasons: [String], otherDetail: String?) } extension CollectionAPI: TargetType { @@ -33,6 +35,8 @@ extension CollectionAPI: TargetType { return "/api/v1/collections/\(collectionId)" case .fetchRecentViewedCollections: return "/api/v1/collections/recent" + case let .reportCollection(collectionId, _, _): + return "/api/v1/collections/\(collectionId)/reports" } } @@ -40,7 +44,7 @@ extension CollectionAPI: TargetType { switch self { case .fetchCollections, .fetchCollectionDetail, .fetchRecentViewedCollections: return .get - case .createCollection: + case .createCollection, .reportCollection: return .post case .updateCollection: return .put @@ -52,22 +56,19 @@ extension CollectionAPI: TargetType { public var task: Moya.Task { switch self { case let .fetchCollections(cursor, size): - var parameters: [String: Any] = [ - "size": size, - ] + var parameters: [String: Any] = ["size": size] if let cursor { parameters["cursor"] = cursor } - return .requestParameters( - parameters: parameters, - encoding: URLEncoding.queryString - ) + return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) case let .createCollection(collectionInfo): return .requestJSONEncodable(collectionInfo) case let .updateCollection(_, collectionInfo): return .requestJSONEncodable(collectionInfo) case .deleteCollection, .fetchCollectionDetail, .fetchRecentViewedCollections: return .requestPlain + case let .reportCollection(_, reasons, otherDetail): + return .requestJSONEncodable(ReportRequestDTO(reasons: reasons, otherDetail: otherDetail)) } } } diff --git a/FLINT/Data/Sources/Networking/Service/CollectionService.swift b/FLINT/Data/Sources/Networking/Service/CollectionService.swift index e299e19..0a40fc0 100644 --- a/FLINT/Data/Sources/Networking/Service/CollectionService.swift +++ b/FLINT/Data/Sources/Networking/Service/CollectionService.swift @@ -22,6 +22,7 @@ public protocol CollectionService { func deleteCollection(collectionId: Int64) -> AnyPublisher func fetchCollectionDetail(collectionId: Int64) -> AnyPublisher func fetchRecentViewedCollections() -> AnyPublisher + func reportCollection(collectionId: Int64, reasons: [String], otherDetail: String?) -> AnyPublisher } public final class DefaultCollectionService: CollectionService { @@ -60,4 +61,9 @@ public final class DefaultCollectionService: CollectionService { return collectionAPIProvider.requestPublisher(.fetchRecentViewedCollections) .mapBaseResponseData(CollectionsDTO.self) } + + public func reportCollection(collectionId: Int64, reasons: [String], otherDetail: String?) -> AnyPublisher { + return collectionAPIProvider.requestPublisher(.reportCollection(collectionId: collectionId, reasons: reasons, otherDetail: otherDetail)) + .mapBaseResponseEmpty() + } } diff --git a/FLINT/Data/Sources/RepositoryImpl/CollectionRepositoryImpl.swift b/FLINT/Data/Sources/RepositoryImpl/CollectionRepositoryImpl.swift index c8cfe05..5881333 100644 --- a/FLINT/Data/Sources/RepositoryImpl/CollectionRepositoryImpl.swift +++ b/FLINT/Data/Sources/RepositoryImpl/CollectionRepositoryImpl.swift @@ -52,4 +52,8 @@ public final class DefaultCollectionRepository: CollectionRepository { .tryMap { try $0.entities } .eraseToAnyPublisher() } + + public func reportCollection(collectionId: Int64, reasons: [String], otherDetail: String?) -> AnyPublisher { + return collectionService.reportCollection(collectionId: collectionId, reasons: reasons, otherDetail: otherDetail) + } } diff --git a/FLINT/Domain/Sources/Repository/CollectionRepository.swift b/FLINT/Domain/Sources/Repository/CollectionRepository.swift index 15bf27c..ca28763 100644 --- a/FLINT/Domain/Sources/Repository/CollectionRepository.swift +++ b/FLINT/Domain/Sources/Repository/CollectionRepository.swift @@ -17,4 +17,5 @@ public protocol CollectionRepository { func deleteCollection(collectionId: Int64) -> AnyPublisher func fetchCollectionDetail(collectionId: Int64) -> AnyPublisher func fetchRecentViewedCollections() -> AnyPublisher<[CollectionEntity], Error> + func reportCollection(collectionId: Int64, reasons: [String], otherDetail: String?) -> AnyPublisher } diff --git a/FLINT/Domain/Sources/UseCase/Collection/ReportCollectionUseCase.swift b/FLINT/Domain/Sources/UseCase/Collection/ReportCollectionUseCase.swift new file mode 100644 index 0000000..b449c37 --- /dev/null +++ b/FLINT/Domain/Sources/UseCase/Collection/ReportCollectionUseCase.swift @@ -0,0 +1,28 @@ +// +// ReportCollectionUseCase.swift +// Domain +// +// Created by 소은 on 6/21/26. +// + +import Combine +import Foundation + +import Repository + +public protocol ReportCollectionUseCase { + func callAsFunction(collectionId: Int64, reasons: [String], otherDetail: String?) -> AnyPublisher +} + +public final class DefaultReportCollectionUseCase: ReportCollectionUseCase { + + private let collectionRepository: CollectionRepository + + public init(collectionRepository: CollectionRepository) { + self.collectionRepository = collectionRepository + } + + public func callAsFunction(collectionId: Int64, reasons: [String], otherDetail: String?) -> AnyPublisher { + return collectionRepository.reportCollection(collectionId: collectionId, reasons: reasons, otherDetail: otherDetail) + } +} diff --git a/FLINT/FLINT/Dependency/DIContainer.swift b/FLINT/FLINT/Dependency/DIContainer.swift index 284bf17..982df87 100644 --- a/FLINT/FLINT/Dependency/DIContainer.swift +++ b/FLINT/FLINT/Dependency/DIContainer.swift @@ -25,6 +25,8 @@ typealias DependencyFactory = ViewControllerFactory & ExploreViewModelFactory & ProfileViewModelFactory & + ReportViewModelFactory & + CreateCollectionViewModelFactory & AddContentSelectViewModelFactory & CollectionFolderListViewModelFactory & diff --git a/FLINT/FLINT/Dependency/Factory/UseCase/Collection/ReportCollectionUseCaseFactory.swift b/FLINT/FLINT/Dependency/Factory/UseCase/Collection/ReportCollectionUseCaseFactory.swift new file mode 100644 index 0000000..9b5fe55 --- /dev/null +++ b/FLINT/FLINT/Dependency/Factory/UseCase/Collection/ReportCollectionUseCaseFactory.swift @@ -0,0 +1,20 @@ +// +// ReportCollectionUseCaseFactory.swift +// FLINT +// +// Created by 소은 on 6/21/26. +// + +import Foundation + +import Domain + +protocol ReportCollectionUseCaseFactory: CollectionRepositoryFactory { + func makeReportCollectionUseCase() -> ReportCollectionUseCase +} + +extension ReportCollectionUseCaseFactory { + func makeReportCollectionUseCase() -> ReportCollectionUseCase { + return DefaultReportCollectionUseCase(collectionRepository: makeCollectionRepository()) + } +} diff --git a/FLINT/FLINT/Dependency/Factory/ViewController/CollectionDetailViewControllerFactory+.swift b/FLINT/FLINT/Dependency/Factory/ViewController/CollectionDetailViewControllerFactory+.swift index 40ee1c1..321f167 100644 --- a/FLINT/FLINT/Dependency/Factory/ViewController/CollectionDetailViewControllerFactory+.swift +++ b/FLINT/FLINT/Dependency/Factory/ViewController/CollectionDetailViewControllerFactory+.swift @@ -11,6 +11,10 @@ import Presentation extension CollectionDetailViewControllerFactory where Self: CollectionDetailViewModelFactory & ViewControllerFactory { func makeCollectionDetailViewController(collectionId: Int64) -> CollectionDetailViewController { - return CollectionDetailViewController(viewModel: makeCollectionDetailViewModel(collectionId: collectionId), viewControllerFactory: self) + let vc = CollectionDetailViewController(viewModel: makeCollectionDetailViewModel(collectionId: collectionId), viewControllerFactory: self) + vc.onTapReport = { collectionId in + let reportVC = vc.viewControllerFactory?.makeReportViewController(collectionId: collectionId) + } + return vc } } diff --git a/FLINT/FLINT/Dependency/Factory/ViewController/ReportViewControllerFactory+.swift b/FLINT/FLINT/Dependency/Factory/ViewController/ReportViewControllerFactory+.swift new file mode 100644 index 0000000..39378e6 --- /dev/null +++ b/FLINT/FLINT/Dependency/Factory/ViewController/ReportViewControllerFactory+.swift @@ -0,0 +1,17 @@ +// +// ReportViewControllerFactory+.swift +// FLINT +// + +import Foundation + +import Presentation + +extension ReportViewControllerFactory where Self: ReportViewModelFactory & ViewControllerFactory { + func makeReportViewController(collectionId: Int64) -> ReportViewController { + return ReportViewController( + viewModel: makeReportViewModel(collectionId: collectionId), + viewControllerFactory: self + ) + } +} diff --git a/FLINT/FLINT/Dependency/Factory/ViewModel/ReportViewModelFactory.swift b/FLINT/FLINT/Dependency/Factory/ViewModel/ReportViewModelFactory.swift new file mode 100644 index 0000000..ca5406a --- /dev/null +++ b/FLINT/FLINT/Dependency/Factory/ViewModel/ReportViewModelFactory.swift @@ -0,0 +1,20 @@ +// +// ReportViewModelFactory.swift +// FLINT +// +// Created by 소은 on 6/21/26. +// + +import Foundation + +import Presentation + +protocol ReportViewModelFactory: ReportCollectionUseCaseFactory { + func makeReportViewModel(collectionId: Int64) -> ReportViewModel +} + +extension ReportViewModelFactory { + func makeReportViewModel(collectionId: Int64) -> ReportViewModel { + return ReportViewModel(reportCollectionUseCase: makeReportCollectionUseCase(), collectionId: collectionId) + } +} diff --git a/FLINT/Presentation/Sources/ViewController/Dependency/ViewControllerFactory.swift b/FLINT/Presentation/Sources/ViewController/Dependency/ViewControllerFactory.swift index 615207b..2533f0a 100644 --- a/FLINT/Presentation/Sources/ViewController/Dependency/ViewControllerFactory.swift +++ b/FLINT/Presentation/Sources/ViewController/Dependency/ViewControllerFactory.swift @@ -18,11 +18,11 @@ public typealias ViewControllerFactory = // MARK: - Onboarding - TermsAgreementViewControllerFactory & NicknameViewControllerFactory & ContentSelectViewControllerFactory & // OttSelectViewControllerFactory & OnboardingDoneViewControllerFactory & + TermsAgreementViewControllerFactory & // MARK: - Main @@ -37,4 +37,5 @@ public typealias ViewControllerFactory = CollectionDetailViewControllerFactory & SavedCollectionListViewControllerFactory & AddContentSelectViewControllerFactory & - CreateCollectionViewControllerFactory + CreateCollectionViewControllerFactory & + ReportViewControllerFactory diff --git a/FLINT/Presentation/Sources/ViewController/ReportViewController.swift b/FLINT/Presentation/Sources/ViewController/ReportViewController.swift index 0ee0235..ea5e93e 100644 --- a/FLINT/Presentation/Sources/ViewController/ReportViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/ReportViewController.swift @@ -11,6 +11,14 @@ import Combine import View import ViewModel +// MARK: - ReportViewControllerFactory + +public protocol ReportViewControllerFactory { + func makeReportViewController(collectionId: Int64) -> ReportViewController +} + +// MARK: - ReportViewController + public final class ReportViewController: BaseViewController { // MARK: - Property @@ -29,10 +37,9 @@ public final class ReportViewController: BaseViewController { // MARK: - Init - public init(viewModel: ReportViewModel) { + public init(viewModel: ReportViewModel, viewControllerFactory: (any ViewControllerFactory)?) { self.viewModel = viewModel - // FIXME: viewControllerFactor 주입 - super.init(viewControllerFactory: nil) + super.init(viewControllerFactory: viewControllerFactory) } required init?(coder: NSCoder) { @@ -74,8 +81,8 @@ public final class ReportViewController: BaseViewController { output.submitSuccess .receive(on: DispatchQueue.main) .sink { [weak self] _ in - print("신고 성공") self?.navigationController?.popViewController(animated: true) + Toast.text("신고가 접수되었어요").show() } .store(in: &cancellables) @@ -98,13 +105,13 @@ public final class ReportViewController: BaseViewController { } private func setupTextViewObserver() { - NotificationCenter.default.addObserver( - self, - selector: #selector(textViewDidBeginEditing), - name: UITextView.textDidBeginEditingNotification, - object: rootView.textView - ) - } + NotificationCenter.default.addObserver( + self, + selector: #selector(textViewDidBeginEditing), + name: UITextView.textDidBeginEditingNotification, + object: rootView.textView + ) + } private func setupActions() { for (index, button) in rootView.radioButtons.enumerated() { @@ -150,7 +157,6 @@ public final class ReportViewController: BaseViewController { selectedRadioIndex = 4 radioSelectionSubject.send(4) } - // MARK: - Custom Method diff --git a/FLINT/Presentation/Sources/ViewController/Scene/CollectionDetail/CollectionDetailViewController.swift b/FLINT/Presentation/Sources/ViewController/Scene/CollectionDetail/CollectionDetailViewController.swift index 45f2e20..a3a794d 100644 --- a/FLINT/Presentation/Sources/ViewController/Scene/CollectionDetail/CollectionDetailViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/Scene/CollectionDetail/CollectionDetailViewController.swift @@ -194,7 +194,9 @@ public final class CollectionDetailViewController: BaseViewController { ) { self.viewModel = viewModel self.fetchOTTPlatformsForContentUseCase = fetchOTTPlatformsForContentUseCase - super.init(nibName: nil, bundle: nil) + super.init(viewControllerFactory: viewControllerFactory) self.viewControllerFactory = viewControllerFactory } diff --git a/FLINT/Presentation/Sources/ViewController/Scene/Onboarding/TermsAgreement/TermsAgreementViewController.swift b/FLINT/Presentation/Sources/ViewController/Scene/Onboarding/TermsAgreement/TermsAgreementViewController.swift index 0c8e770..e4c5af1 100644 --- a/FLINT/Presentation/Sources/ViewController/Scene/Onboarding/TermsAgreement/TermsAgreementViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/Scene/Onboarding/TermsAgreement/TermsAgreementViewController.swift @@ -28,6 +28,7 @@ public final class TermsAgreementViewController: BaseViewController() - private var selectedRadioIndex: Int? + + private static let reasonKeys = ["ABUSE", "OBSCENE", "SPAM", "COPYRIGHT", "OTHER"] + + private var selectedRadioIndex: Int = -1 private var textInput: String = "" // MARK: - Init - public init() {} + public init(reportCollectionUseCase: ReportCollectionUseCase, collectionId: Int64) { + self.reportCollectionUseCase = reportCollectionUseCase + self.collectionId = collectionId + } // MARK: - Input @@ -50,15 +60,11 @@ public final class ReportViewModel { public func transform(input: Input) -> Output { input.radioSelected - .sink { [weak self] index in - self?.selectedRadioIndex = index - } + .sink { [weak self] index in self?.selectedRadioIndex = index } .store(in: &cancellables) input.textInput - .sink { [weak self] text in - self?.textInput = text - } + .sink { [weak self] text in self?.textInput = text } .store(in: &cancellables) let isSubmitEnabledPublisher = Publishers.CombineLatest( @@ -66,39 +72,36 @@ public final class ReportViewModel { input.textInput.prepend("") ) .map { radioIndex, text -> Bool in - if radioIndex == -1 { - return false - } - if radioIndex == 4 { - return !text.isEmpty - } + if radioIndex == -1 { return false } + if radioIndex == 4 { return !text.isEmpty } return true } .eraseToAnyPublisher() let submitResult = input.submitButtonTapped .flatMap { [weak self] _ -> AnyPublisher, Never> in - guard let self = self else { + guard let self, self.selectedRadioIndex >= 0 else { return Empty().eraseToAnyPublisher() } - return self.submitReport() + let reason = Self.reasonKeys[self.selectedRadioIndex] + let otherDetail = self.selectedRadioIndex == 4 ? self.textInput : nil + return self.reportCollectionUseCase(collectionId: self.collectionId, reasons: [reason], otherDetail: otherDetail) + .map { Result.success($0) } + .catch { Just(Result.failure($0)) } + .eraseToAnyPublisher() } .share() let submitSuccess = submitResult .compactMap { result -> Void? in - if case .success = result { - return () - } + if case .success = result { return () } return nil } .eraseToAnyPublisher() let submitError = submitResult .compactMap { result -> Error? in - if case .failure(let error) = result { - return error - } + if case .failure(let error) = result { return error } return nil } .eraseToAnyPublisher() @@ -109,14 +112,4 @@ public final class ReportViewModel { submitError: submitError ) } - - // MARK: - Custom Method - - private func submitReport() -> AnyPublisher, Never> { - // TODO: UseCase 호출 - - // 임시 구현 - return Just(Result.success(())) - .eraseToAnyPublisher() - } }