From 9fa906156bf0e7c8c8b415b743e9515179ca8ad2 Mon Sep 17 00:00:00 2001 From: soonny <134983918+soeun11@users.noreply.github.com> Date: Sun, 21 Jun 2026 02:27:35 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[network]=20=EC=8B=A0=EA=B3=A0=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Sources/DTO/Base/BaseResponse.swift | 4 +- .../DTO/Collection/ReportRequestDTO.swift | 16 ++++++ .../Networking/API/CollectionAPI.swift | 17 +++--- .../Service/CollectionService.swift | 8 +++ .../CollectionRepositoryImpl.swift | 4 ++ .../Repository/CollectionRepository.swift | 1 + .../Collection/ReportCollectionUseCase.swift | 28 +++++++++ FLINT/FLINT/Dependency/DIContainer.swift | 2 + .../ReportCollectionUseCaseFactory.swift | 20 +++++++ .../ReportViewControllerFactory+.swift | 17 ++++++ .../ViewModel/ReportViewModelFactory.swift | 20 +++++++ .../Dependency/ViewControllerFactory.swift | 5 +- .../ViewController/ReportViewController.swift | 28 +++++---- .../TermsAgreementViewController.swift | 4 +- .../Sources/ViewModel/ReportViewModel.swift | 57 ++++++++----------- 15 files changed, 174 insertions(+), 57 deletions(-) create mode 100644 FLINT/Data/Sources/DTO/Collection/ReportRequestDTO.swift create mode 100644 FLINT/Domain/Sources/UseCase/Collection/ReportCollectionUseCase.swift create mode 100644 FLINT/FLINT/Dependency/Factory/UseCase/Collection/ReportCollectionUseCaseFactory.swift create mode 100644 FLINT/FLINT/Dependency/Factory/ViewController/ReportViewControllerFactory+.swift create mode 100644 FLINT/FLINT/Dependency/Factory/ViewModel/ReportViewModelFactory.swift diff --git a/FLINT/Data/Sources/DTO/Base/BaseResponse.swift b/FLINT/Data/Sources/DTO/Base/BaseResponse.swift index cd097c8a..3a1832df 100644 --- a/FLINT/Data/Sources/DTO/Base/BaseResponse.swift +++ b/FLINT/Data/Sources/DTO/Base/BaseResponse.swift @@ -19,7 +19,7 @@ public struct BaseResponse: Codable { public let additionalInfo: [String: String]? public let status: Int - public let message: String + public let message: String? public let data: T? } @@ -36,7 +36,7 @@ extension BaseResponse { errorCode: errorCode ?? "", additionalInfo: additionalInfo ?? [:], status: status, - message: message + message: message ?? "" ) } } diff --git a/FLINT/Data/Sources/DTO/Collection/ReportRequestDTO.swift b/FLINT/Data/Sources/DTO/Collection/ReportRequestDTO.swift new file mode 100644 index 00000000..e4a4130a --- /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 01e41785..afa68cc6 100644 --- a/FLINT/Data/Sources/Networking/API/CollectionAPI.swift +++ b/FLINT/Data/Sources/Networking/API/CollectionAPI.swift @@ -10,12 +10,14 @@ import Foundation import Moya import Domain +import DTO public enum CollectionAPI { case fetchCollections(cursor: Int64?, size: Int32) case createCollection(collectionInfo: CreateCollectionEntity) case fetchCollectionDetail(collectionId: Int64) case fetchRecentViewedCollections + case reportCollection(collectionId: Int64, reasons: [String], otherDetail: String?) } extension CollectionAPI: TargetType { @@ -27,6 +29,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" } } @@ -34,7 +38,7 @@ extension CollectionAPI: TargetType { switch self { case .fetchCollections, .fetchCollectionDetail, .fetchRecentViewedCollections: return .get - case .createCollection: + case .createCollection, .reportCollection: return .post } } @@ -42,20 +46,17 @@ 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 .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 0e4c26d8..612281c4 100644 --- a/FLINT/Data/Sources/Networking/Service/CollectionService.swift +++ b/FLINT/Data/Sources/Networking/Service/CollectionService.swift @@ -20,6 +20,7 @@ public protocol CollectionService { func createCollection(collectionInfo: CreateCollectionEntity) -> AnyPublisher func fetchCollectionDetail(collectionId: Int64) -> AnyPublisher func fetchRecentViewedCollections() -> AnyPublisher + func reportCollection(collectionId: Int64, reasons: [String], otherDetail: String?) -> AnyPublisher } public final class DefaultCollectionService: CollectionService { @@ -48,4 +49,11 @@ 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)) + .mapBaseResponseData(BlankData.self) + .map { _ in } + .eraseToAnyPublisher() + } } diff --git a/FLINT/Data/Sources/RepositoryImpl/CollectionRepositoryImpl.swift b/FLINT/Data/Sources/RepositoryImpl/CollectionRepositoryImpl.swift index be2dd5ca..187a1500 100644 --- a/FLINT/Data/Sources/RepositoryImpl/CollectionRepositoryImpl.swift +++ b/FLINT/Data/Sources/RepositoryImpl/CollectionRepositoryImpl.swift @@ -44,4 +44,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 8d60e639..4b4af4d1 100644 --- a/FLINT/Domain/Sources/Repository/CollectionRepository.swift +++ b/FLINT/Domain/Sources/Repository/CollectionRepository.swift @@ -15,4 +15,5 @@ public protocol CollectionRepository { func createCollection(collectionInfo: CreateCollectionEntity) -> 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 00000000..b449c37a --- /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 4c842490..73b25ea2 100644 --- a/FLINT/FLINT/Dependency/DIContainer.swift +++ b/FLINT/FLINT/Dependency/DIContainer.swift @@ -24,6 +24,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 00000000..9b5fe553 --- /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/ReportViewControllerFactory+.swift b/FLINT/FLINT/Dependency/Factory/ViewController/ReportViewControllerFactory+.swift new file mode 100644 index 00000000..39378e6a --- /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 00000000..ca5406a1 --- /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 2ed18798..098135ec 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 @@ -36,4 +36,5 @@ public typealias ViewControllerFactory = CollectionFolderListViewControllerFactory & CollectionDetailViewControllerFactory & AddContentSelectViewControllerFactory & - CreateCollectionViewControllerFactory + CreateCollectionViewControllerFactory & + ReportViewControllerFactory diff --git a/FLINT/Presentation/Sources/ViewController/ReportViewController.swift b/FLINT/Presentation/Sources/ViewController/ReportViewController.swift index 0ee0235e..223cfa70 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) { @@ -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/Onboarding/TermsAgreement/TermsAgreementViewController.swift b/FLINT/Presentation/Sources/ViewController/Scene/Onboarding/TermsAgreement/TermsAgreementViewController.swift index 15f959b1..e4c5af1e 100644 --- a/FLINT/Presentation/Sources/ViewController/Scene/Onboarding/TermsAgreement/TermsAgreementViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/Scene/Onboarding/TermsAgreement/TermsAgreementViewController.swift @@ -28,10 +28,10 @@ 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() - } } From b662b40519dc1a2a37d59cefb57f73e0dfcd24fe Mon Sep 17 00:00:00 2001 From: soonny <134983918+soeun11@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:45:23 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[feat]=20=EC=8B=A0=EA=B3=A0=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EB=B7=B0=20=EC=97=B0=EA=B2=B0=20=20+=20=ED=86=A0?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=84=B8=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Sources/Networking/Service/CollectionService.swift | 4 +--- .../CollectionDetailViewControllerFactory+.swift | 6 +++++- .../Sources/ViewController/ReportViewController.swift | 2 +- .../CollectionDetail/CollectionDetailViewController.swift | 4 +++- .../ViewController/Scene/Home/HomeViewController.swift | 2 +- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/FLINT/Data/Sources/Networking/Service/CollectionService.swift b/FLINT/Data/Sources/Networking/Service/CollectionService.swift index e25a3718..0a40fc07 100644 --- a/FLINT/Data/Sources/Networking/Service/CollectionService.swift +++ b/FLINT/Data/Sources/Networking/Service/CollectionService.swift @@ -64,8 +64,6 @@ public final class DefaultCollectionService: CollectionService { public func reportCollection(collectionId: Int64, reasons: [String], otherDetail: String?) -> AnyPublisher { return collectionAPIProvider.requestPublisher(.reportCollection(collectionId: collectionId, reasons: reasons, otherDetail: otherDetail)) - .mapBaseResponseData(BlankData.self) - .map { _ in } - .eraseToAnyPublisher() + .mapBaseResponseEmpty() } } diff --git a/FLINT/FLINT/Dependency/Factory/ViewController/CollectionDetailViewControllerFactory+.swift b/FLINT/FLINT/Dependency/Factory/ViewController/CollectionDetailViewControllerFactory+.swift index 40ee1c14..321f1676 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/Presentation/Sources/ViewController/ReportViewController.swift b/FLINT/Presentation/Sources/ViewController/ReportViewController.swift index 223cfa70..ea5e93ee 100644 --- a/FLINT/Presentation/Sources/ViewController/ReportViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/ReportViewController.swift @@ -81,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) diff --git a/FLINT/Presentation/Sources/ViewController/Scene/CollectionDetail/CollectionDetailViewController.swift b/FLINT/Presentation/Sources/ViewController/Scene/CollectionDetail/CollectionDetailViewController.swift index 45f2e20e..a3a794dd 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 }