diff --git a/FLINT/Data/Sources/DTO/Auth/LogoutRequestDTO.swift b/FLINT/Data/Sources/DTO/Auth/LogoutRequestDTO.swift new file mode 100644 index 00000000..6ed99095 --- /dev/null +++ b/FLINT/Data/Sources/DTO/Auth/LogoutRequestDTO.swift @@ -0,0 +1,16 @@ +// +// LogoutRequestDTO.swift +// Data +// +// Created by 소은 on 6/19/26. +// + +import Foundation + +public struct LogoutRequestDTO: Encodable { + public let refreshToken: String + + public init(refreshToken: String) { + self.refreshToken = refreshToken + } +} diff --git a/FLINT/Data/Sources/DTO/Auth/WithdrawRequestDTO.swift b/FLINT/Data/Sources/DTO/Auth/WithdrawRequestDTO.swift new file mode 100644 index 00000000..992d2e40 --- /dev/null +++ b/FLINT/Data/Sources/DTO/Auth/WithdrawRequestDTO.swift @@ -0,0 +1,14 @@ +// +// WithdrawRequestDTO.swift +// Data +// + +import Foundation + +public struct WithdrawRequestDTO: Encodable { + public let agreedTermsIds: [String] + + public init(agreedTermsIds: [String]) { + self.agreedTermsIds = agreedTermsIds + } +} 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/AuthAPI.swift b/FLINT/Data/Sources/Networking/API/AuthAPI.swift index 1f28956c..9f38f3d9 100644 --- a/FLINT/Data/Sources/Networking/API/AuthAPI.swift +++ b/FLINT/Data/Sources/Networking/API/AuthAPI.swift @@ -12,12 +12,12 @@ import Moya import DTO public enum AuthAPI { - case logout + case logout(refreshToken: String) case logoutAll case refresh case signup(userInfo: SignupRequestDTO) case socialVerify(socialAuthCredential: SocialVerifyRequestDTO) - case withdraw + case withdraw(agreedTermsIds: [String]) } extension AuthAPI: TargetType { @@ -25,7 +25,9 @@ extension AuthAPI: TargetType { switch self { case .signup: return "/api/v1/auth/signup" - case .logout, .logoutAll, .refresh: + case .logout: + return "/api/v1/auth/logout" + case .logoutAll, .refresh: #warning("TODO: - 나중에 구현할 것") return "TODO" case .socialVerify: @@ -50,8 +52,12 @@ extension AuthAPI: TargetType { return .requestJSONEncodable(userInfo) case let .socialVerify(socialAuthCredential): return .requestJSONEncodable(socialAuthCredential) - case .logout, .logoutAll, .refresh, .withdraw: + case let .logout(refreshToken): + return .requestJSONEncodable(LogoutRequestDTO(refreshToken: refreshToken)) + case .logoutAll, .refresh: return .requestPlain + case let .withdraw(agreedTermsIds): + return .requestJSONEncodable(WithdrawRequestDTO(agreedTermsIds: agreedTermsIds)) } } } diff --git a/FLINT/Data/Sources/Networking/API/CollectionAPI.swift b/FLINT/Data/Sources/Networking/API/CollectionAPI.swift index c5a50aae..0e07b573 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/AuthService.swift b/FLINT/Data/Sources/Networking/Service/AuthService.swift index be999c37..ce41afe2 100644 --- a/FLINT/Data/Sources/Networking/Service/AuthService.swift +++ b/FLINT/Data/Sources/Networking/Service/AuthService.swift @@ -18,7 +18,8 @@ import DTO public protocol AuthService { func signup(userInfo: SignupInfoEntity) -> AnyPublisher func socialVerify(socialAuthCredential: SocialVerifyRequestDTO) -> AnyPublisher - func withDraw() -> AnyPublisher + func logout() -> AnyPublisher + func withDraw(agreedTermsIds: [String]) -> AnyPublisher } public final class DefaultAuthService: AuthService { @@ -67,10 +68,24 @@ public final class DefaultAuthService: AuthService { .eraseToAnyPublisher() } - public func withDraw() -> AnyPublisher { - authAPIProvider.requestPublisher(.withdraw) - .mapBaseResponseData(BlankData.self) - .map({ _ in }) + public func logout() -> AnyPublisher { + guard let refreshToken = tokenStorage.load(type: .refreshToken) else { + return Fail(error: TokenError.noToken).eraseToAnyPublisher() + } + return authAPIProvider.requestPublisher(.logout(refreshToken: refreshToken)) + .logged() + .tryMap { [weak self] response in + guard (200..<300).contains(response.statusCode) else { + throw MoyaError.statusCode(response) + } + self?.tokenStorage.clearAll() + } .eraseToAnyPublisher() } + + public func withDraw(agreedTermsIds: [String]) -> AnyPublisher { + return authAPIProvider.requestPublisher(.withdraw(agreedTermsIds: agreedTermsIds)) + .logged() + .mapBaseResponseEmpty() + } } diff --git a/FLINT/Data/Sources/Networking/Service/CollectionService.swift b/FLINT/Data/Sources/Networking/Service/CollectionService.swift index e299e19c..e25a3718 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,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/AuthRepositoryImpl.swift b/FLINT/Data/Sources/RepositoryImpl/AuthRepositoryImpl.swift index c7647ec8..b61f3863 100644 --- a/FLINT/Data/Sources/RepositoryImpl/AuthRepositoryImpl.swift +++ b/FLINT/Data/Sources/RepositoryImpl/AuthRepositoryImpl.swift @@ -33,7 +33,11 @@ public final class DefaultAuthRepository: AuthRepository { .eraseToAnyPublisher() } - public func withDraw() -> AnyPublisher { - return authService.withDraw() + public func logout() -> AnyPublisher { + return authService.logout() + } + + public func withDraw(agreedTermsIds: [String]) -> AnyPublisher { + return authService.withDraw(agreedTermsIds: agreedTermsIds) } } diff --git a/FLINT/Data/Sources/RepositoryImpl/CollectionRepositoryImpl.swift b/FLINT/Data/Sources/RepositoryImpl/CollectionRepositoryImpl.swift index c8cfe053..58813331 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/AuthRepository.swift b/FLINT/Domain/Sources/Repository/AuthRepository.swift index 7ab15d08..52ccf0a6 100644 --- a/FLINT/Domain/Sources/Repository/AuthRepository.swift +++ b/FLINT/Domain/Sources/Repository/AuthRepository.swift @@ -13,5 +13,6 @@ import Entity public protocol AuthRepository { func signup(userInfo: SignupInfoEntity) -> AnyPublisher func socialVerify(socialAuthCredential: SocialVerifyEntity) -> AnyPublisher - func withDraw() -> AnyPublisher + func logout() -> AnyPublisher + func withDraw(agreedTermsIds: [String]) -> AnyPublisher } diff --git a/FLINT/Domain/Sources/Repository/CollectionRepository.swift b/FLINT/Domain/Sources/Repository/CollectionRepository.swift index 15bf27c0..ca287631 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/Auth/LogoutUseCase.swift b/FLINT/Domain/Sources/UseCase/Auth/LogoutUseCase.swift new file mode 100644 index 00000000..6880c3d7 --- /dev/null +++ b/FLINT/Domain/Sources/UseCase/Auth/LogoutUseCase.swift @@ -0,0 +1,28 @@ +// +// LogoutUseCase.swift +// Domain +// +// Created by 소은 on 6/19/26. +// + +import Combine +import Foundation + +import Repository + +public protocol LogoutUseCase { + func callAsFunction() -> AnyPublisher +} + +public final class DefaultLogoutUseCase: LogoutUseCase { + + private let authRepository: AuthRepository + + public init(authRepository: AuthRepository) { + self.authRepository = authRepository + } + + public func callAsFunction() -> AnyPublisher { + return authRepository.logout() + } +} diff --git a/FLINT/Domain/Sources/UseCase/Auth/WithDrawUseCase.swift b/FLINT/Domain/Sources/UseCase/Auth/WithDrawUseCase.swift index afe2fdec..0c27fc9f 100644 --- a/FLINT/Domain/Sources/UseCase/Auth/WithDrawUseCase.swift +++ b/FLINT/Domain/Sources/UseCase/Auth/WithDrawUseCase.swift @@ -12,7 +12,7 @@ import Entity import Repository public protocol WithDrawUseCase { - func callAsFunction() -> AnyPublisher + func callAsFunction(agreedTermsIds: [String]) -> AnyPublisher } public final class DefaultWithDrawUseCase: WithDrawUseCase { @@ -23,7 +23,7 @@ public final class DefaultWithDrawUseCase: WithDrawUseCase { self.authRepository = authRepository } - public func callAsFunction() -> AnyPublisher { - return authRepository.withDraw() + public func callAsFunction(agreedTermsIds: [String]) -> AnyPublisher { + return authRepository.withDraw(agreedTermsIds: agreedTermsIds) } } 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 284bf17a..23ecd96c 100644 --- a/FLINT/FLINT/Dependency/DIContainer.swift +++ b/FLINT/FLINT/Dependency/DIContainer.swift @@ -24,6 +24,9 @@ typealias DependencyFactory = ViewControllerFactory & FetchOTTPlatformsForContentUseCaseFactory & ExploreViewModelFactory & ProfileViewModelFactory & + + SettingViewModelFactory & + WithdrawViewModelFactory & CreateCollectionViewModelFactory & AddContentSelectViewModelFactory & diff --git a/FLINT/FLINT/Dependency/Factory/UseCase/Auth/LogoutUseCaseFactory..swift b/FLINT/FLINT/Dependency/Factory/UseCase/Auth/LogoutUseCaseFactory..swift new file mode 100644 index 00000000..4c094e68 --- /dev/null +++ b/FLINT/FLINT/Dependency/Factory/UseCase/Auth/LogoutUseCaseFactory..swift @@ -0,0 +1,20 @@ +// +// LogoutUseCaseFactory..swift +// FLINT +// +// Created by 소은 on 6/19/26. +// + +import Foundation + +import Domain + +protocol LogoutUseCaseFactory: AuthRepositoryFactory { + func makeLogoutUseCase() -> LogoutUseCase +} + +extension LogoutUseCaseFactory { + func makeLogoutUseCase() -> LogoutUseCase { + return DefaultLogoutUseCase(authRepository: makeAuthRepository()) + } +} 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/ViewController/SettingViewControllerFactory+.swift b/FLINT/FLINT/Dependency/Factory/ViewController/SettingViewControllerFactory+.swift new file mode 100644 index 00000000..a45bd3df --- /dev/null +++ b/FLINT/FLINT/Dependency/Factory/ViewController/SettingViewControllerFactory+.swift @@ -0,0 +1,19 @@ +// +// SettingViewControllerFactory+.swift +// FLINT +// +// Created by 소은 on 6/19/26. +// + +import Foundation + +import Presentation + +extension SettingViewControllerFactory where Self: SettingViewModelFactory & ViewControllerFactory { + func makeSettingViewController() -> SettingViewController { + return SettingViewController( + settingViewModel: makeSettingViewModel(), + viewControllerFactory: self + ) + } +} diff --git a/FLINT/FLINT/Dependency/Factory/ViewController/WithdrawalViewControllerFactory+.swift b/FLINT/FLINT/Dependency/Factory/ViewController/WithdrawalViewControllerFactory+.swift new file mode 100644 index 00000000..0134366a --- /dev/null +++ b/FLINT/FLINT/Dependency/Factory/ViewController/WithdrawalViewControllerFactory+.swift @@ -0,0 +1,19 @@ +// +// WithdrawalViewControllerFactory+.swift +// FLINT +// +// Created by 소은 on 6/19/26. +// + +import Foundation + +import Presentation + +extension WithdrawalViewControllerFactory where Self: WithdrawViewModelFactory & ViewControllerFactory { + func makeWithdrawalViewController() -> WithdrawalViewController { + return WithdrawalViewController( + viewModel: makeWithdrawViewModel(), + 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/FLINT/Dependency/Factory/ViewModel/SettingViewModelFactory.swift b/FLINT/FLINT/Dependency/Factory/ViewModel/SettingViewModelFactory.swift new file mode 100644 index 00000000..4c952200 --- /dev/null +++ b/FLINT/FLINT/Dependency/Factory/ViewModel/SettingViewModelFactory.swift @@ -0,0 +1,20 @@ +// +// SettingViewModelFactory.swift +// FLINT +// +// Created by 소은 on 6/19/26. +// + +import Foundation + +import Presentation + +protocol SettingViewModelFactory: LogoutUseCaseFactory { + func makeSettingViewModel() -> any SettingViewModel +} + +extension SettingViewModelFactory { + func makeSettingViewModel() -> any SettingViewModel { + return DefaultSettingViewModel(logoutUseCase: makeLogoutUseCase()) + } +} diff --git a/FLINT/FLINT/Dependency/Factory/ViewModel/WithdrawViewModelFactory.swift b/FLINT/FLINT/Dependency/Factory/ViewModel/WithdrawViewModelFactory.swift new file mode 100644 index 00000000..263b066b --- /dev/null +++ b/FLINT/FLINT/Dependency/Factory/ViewModel/WithdrawViewModelFactory.swift @@ -0,0 +1,19 @@ +// +// WithdrawViewModelFactory.swift +// FLINT +// +// Created by 소은 on 6/19/26. +// + +import Foundation +import Presentation + +protocol WithdrawViewModelFactory: WithDrawUseCaseFactory { + func makeWithdrawViewModel() -> WithdrawViewModel +} + +extension WithdrawViewModelFactory { + func makeWithdrawViewModel() -> WithdrawViewModel { + return WithdrawViewModel(withDrawUseCase: makeWithDrawUseCase()) + } +} diff --git a/FLINT/Presentation/Sources/ViewController/Dependency/ViewControllerFactory.swift b/FLINT/Presentation/Sources/ViewController/Dependency/ViewControllerFactory.swift index 615207b3..f8e14d0d 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,6 @@ public typealias ViewControllerFactory = CollectionDetailViewControllerFactory & SavedCollectionListViewControllerFactory & AddContentSelectViewControllerFactory & - CreateCollectionViewControllerFactory + CreateCollectionViewControllerFactory & + SettingViewControllerFactory & + WithdrawalViewControllerFactory 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/Home/HomeViewController.swift b/FLINT/Presentation/Sources/ViewController/Scene/Home/HomeViewController.swift index ce15e83e..915d7bb9 100644 --- a/FLINT/Presentation/Sources/ViewController/Scene/Home/HomeViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/Scene/Home/HomeViewController.swift @@ -32,7 +32,7 @@ public final class HomeViewController: BaseViewController { ) { self.viewModel = viewModel self.fetchOTTPlatformsForContentUseCase = fetchOTTPlatformsForContentUseCase - super.init(nibName: nil, bundle: nil) + super.init(viewControllerFactory: viewControllerFactory) self.viewControllerFactory = viewControllerFactory } @@ -109,7 +109,7 @@ public final class HomeViewController: BaseViewController { title: "이 작품을 볼 수 있는 OTT", content: .ott(platforms: platforms) ) - present(vc, animated: false) + present(vc, animated: true) } private func fetchAndPresentOTT(contentId: Int64) { @@ -246,9 +246,10 @@ extension HomeViewController: UITableViewDataSource { cell.onTapItem = { [weak self] content in guard let self else { return } - guard let contentId = Int64(content.id) else { return } - - self.fetchAndPresentOTT(contentId: contentId) + let platforms: [OTTPlatform] = content.ottList.compactMap { ott in + OTTPlatform.fromServerName(ott.ottName) + } + self.presentOTTBottomSheet(platforms: platforms) } return cell diff --git a/FLINT/Presentation/Sources/ViewController/Scene/Onboarding/TermsAgreement/TermsAgreementViewController.swift b/FLINT/Presentation/Sources/ViewController/Scene/Onboarding/TermsAgreement/TermsAgreementViewController.swift index 0c8e770d..e4c5af1e 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 func didTapSetting() { - // TODO: SettingViewController push (DI에 SettingViewControllerFactory 등록 후 연결) - print("setting tapped") + guard let factory = viewControllerFactory else { return } + let settingVC = factory.makeSettingViewController() + navigationController?.pushViewController(settingVC, animated: true) } private func setupTableView() { diff --git a/FLINT/Presentation/Sources/ViewController/Scene/Setting/SettingViewController.swift b/FLINT/Presentation/Sources/ViewController/Scene/Setting/SettingViewController.swift index d189c45c..527ee355 100644 --- a/FLINT/Presentation/Sources/ViewController/Scene/Setting/SettingViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/Scene/Setting/SettingViewController.swift @@ -314,7 +314,8 @@ private extension SettingViewController { } func showWithdrawalAlert() { - print("Navigate to Withdrawal Page") + guard let vc = viewControllerFactory?.makeWithdrawalViewController() else { return } + navigationController?.pushViewController(vc, animated: true) } func handleLogoutSuccess() { diff --git a/FLINT/Presentation/Sources/ViewController/WithdrawViewController.swift b/FLINT/Presentation/Sources/ViewController/WithdrawViewController.swift index 6b1b2c62..58f1f2ad 100644 --- a/FLINT/Presentation/Sources/ViewController/WithdrawViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/WithdrawViewController.swift @@ -12,6 +12,10 @@ import Combine import View import ViewModel +public protocol WithdrawalViewControllerFactory { + func makeWithdrawalViewController() -> WithdrawalViewController +} + public final class WithdrawalViewController: BaseViewController { // MARK: - Property @@ -23,10 +27,9 @@ public final class WithdrawalViewController: BaseViewController // MARK: - Init - public init(viewModel: WithdrawViewModel) { + public init(viewModel: WithdrawViewModel, viewControllerFactory: (any ViewControllerFactory)?) { self.viewModel = viewModel - // FIXME: viewControllerFactory 주입해주기 - super.init(viewControllerFactory: nil) + super.init(viewControllerFactory: viewControllerFactory) } required init?(coder: NSCoder) { @@ -65,8 +68,15 @@ public final class WithdrawalViewController: BaseViewController output.withdrawSuccess .receive(on: DispatchQueue.main) .sink { [weak self] _ in - print("탈퇴 성공") - self?.navigationController?.popViewController(animated: true) + guard let self else { return } + guard let loginVC = viewControllerFactory?.makeLoginViewController() else { return } + let nav = UINavigationController(rootViewController: loginVC) + nav.modalPresentationStyle = .fullScreen + if let window = view.window { + UIView.transition(with: window, duration: 0.3, options: .transitionCrossDissolve) { + window.rootViewController = nav + } + } } .store(in: &cancellables) @@ -78,7 +88,7 @@ public final class WithdrawalViewController: BaseViewController .store(in: &cancellables) } - // MARK: - Setup + // MARK: - Setup private func setupNavigationBar() { setNavigationBar(.init( diff --git a/FLINT/Presentation/Sources/ViewModel/ReportViewModel.swift b/FLINT/Presentation/Sources/ViewModel/ReportViewModel.swift index b08d5b86..154aa7b2 100644 --- a/FLINT/Presentation/Sources/ViewModel/ReportViewModel.swift +++ b/FLINT/Presentation/Sources/ViewModel/ReportViewModel.swift @@ -8,17 +8,27 @@ import Foundation import Combine +import Domain + public final class ReportViewModel { // MARK: - Property + private let reportCollectionUseCase: ReportCollectionUseCase + private let collectionId: Int64 private var cancellables = Set() - 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() - } } diff --git a/FLINT/Presentation/Sources/ViewModel/Scene/Setting/SettingViewModel.swift b/FLINT/Presentation/Sources/ViewModel/Scene/Setting/SettingViewModel.swift index 6ce10397..87b7cda2 100644 --- a/FLINT/Presentation/Sources/ViewModel/Scene/Setting/SettingViewModel.swift +++ b/FLINT/Presentation/Sources/ViewModel/Scene/Setting/SettingViewModel.swift @@ -55,12 +55,13 @@ public final class DefaultSettingViewModel: SettingViewModel { // MARK: - Properties + private let logoutUseCase: LogoutUseCase private var cancellables: Set = Set() // MARK: - Initialization - public init() { - // TODO: UseCase 주입 + public init(logoutUseCase: LogoutUseCase) { + self.logoutUseCase = logoutUseCase fetchUserProfile() } @@ -91,24 +92,33 @@ public final class DefaultSettingViewModel: SettingViewModel { } public func performLogout() { - // TODO: UseCase 연결 - logoutSuccess.send() + logoutUseCase() + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { completion in + if case .failure(let error) = completion { + print("로그아웃 실패: \(error)") + } + }, + receiveValue: { [weak self] in + self?.logoutSuccess.send() + } + ) + .store(in: &cancellables) } public func performWithdrawal() { - // TODO: 탈퇴 UseCase 연결 print("회원탈퇴") } // MARK: - Private Methods private func fetchUserProfile() { - // 임시 Mock 데이터 let mockProfile = UserProfileEntity( id: "user123", nickname: "플리니", profileImageUrl: nil, - role: .fliner + role: .fliner ) userProfile.send(mockProfile) } diff --git a/FLINT/Presentation/Sources/ViewModel/WithdrawViewModel.swift b/FLINT/Presentation/Sources/ViewModel/WithdrawViewModel.swift index 2941606b..689d5b98 100644 --- a/FLINT/Presentation/Sources/ViewModel/WithdrawViewModel.swift +++ b/FLINT/Presentation/Sources/ViewModel/WithdrawViewModel.swift @@ -5,19 +5,23 @@ // Created by 소은 on 5/13/26. // - import Foundation import Combine +import Domain + public final class WithdrawViewModel { - + // MARK: - Property - + + private let withDrawUseCase: WithDrawUseCase private var cancellables = Set() - + // MARK: - Init - - public init() {} + + public init(withDrawUseCase: WithDrawUseCase) { + self.withDrawUseCase = withDrawUseCase + } // MARK: - Input @@ -85,10 +89,9 @@ public final class WithdrawViewModel { // MARK: - Custom Method private func withdraw() -> AnyPublisher, Never> { - // TODO: UseCase 호출 - - // 임시 구현 - return Just(Result.success(())) + return withDrawUseCase(agreedTermsIds: ["10"]) + .map { Result.success($0) } + .catch { Just(Result.failure($0)) } .eraseToAnyPublisher() } }