From af9e543f497c13fef473d6f90b320e1c2151fb71 Mon Sep 17 00:00:00 2001 From: baegteun Date: Thu, 27 Jul 2023 20:01:52 +0900 Subject: [PATCH 01/14] =?UTF-8?q?:seedling:=20::=20[#122]=20DetailNoticeFe?= =?UTF-8?q?ature=20/=20=EA=B8=B0=EB=B3=B8=20=ED=8C=8C=EC=9D=BC=20=EC=84=B8?= =?UTF-8?q?=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ModulePaths.swift | 1 + .../Demo/Resources/LaunchScreen.storyboard | 25 ++++++++++++++ .../Demo/Sources/AppDelegate.swift | 23 +++++++++++++ .../Feature/DetailNoticeFeature/Project.swift | 24 ++++++++++++++ .../Sources/Scene/DetailNoticeStore.swift | 33 +++++++++++++++++++ .../Scene/DetailNoticeViewController.swift | 5 +++ .../Tests/DetailNoticeFeatureTest.swift | 11 +++++++ Projects/Feature/NoticeFeature/Project.swift | 1 + 8 files changed, 123 insertions(+) create mode 100644 Projects/Feature/DetailNoticeFeature/Demo/Resources/LaunchScreen.storyboard create mode 100644 Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift create mode 100644 Projects/Feature/DetailNoticeFeature/Project.swift create mode 100644 Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift create mode 100644 Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift create mode 100644 Projects/Feature/DetailNoticeFeature/Tests/DetailNoticeFeatureTest.swift diff --git a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift index 959a2a19..023ae156 100644 --- a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift +++ b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift @@ -25,6 +25,7 @@ public extension ModulePaths { public extension ModulePaths { enum Feature: String, MicroTargetPathConvertable { + case DetailNoticeFeature case MyViolationListFeature case SplashFeature case ConfirmationDialogFeature diff --git a/Projects/Feature/DetailNoticeFeature/Demo/Resources/LaunchScreen.storyboard b/Projects/Feature/DetailNoticeFeature/Demo/Resources/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/Projects/Feature/DetailNoticeFeature/Demo/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift new file mode 100644 index 00000000..ef75667f --- /dev/null +++ b/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift @@ -0,0 +1,23 @@ +import Inject +import UIKit +@testable import DetailNoticeFeature + +@main +final class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + window = UIWindow(frame: UIScreen.main.bounds) + let store = DetailNoticeStore() + let viewController = DetailNoticeViewController(store: store) + window?.rootViewController = Inject.ViewControllerHost( + UINavigationController(rootViewController: viewController) + ) + window?.makeKeyAndVisible() + + return true + } +} diff --git a/Projects/Feature/DetailNoticeFeature/Project.swift b/Projects/Feature/DetailNoticeFeature/Project.swift new file mode 100644 index 00000000..e84fe776 --- /dev/null +++ b/Projects/Feature/DetailNoticeFeature/Project.swift @@ -0,0 +1,24 @@ +import ProjectDescription +import ProjectDescriptionHelpers +import DependencyPlugin + +let project = Project.module( + name: ModulePaths.Feature.DetailNoticeFeature.rawValue, + targets: [ + .implements(module: .feature(.DetailNoticeFeature), dependencies: [ + .feature(target: .BaseFeature), + .domain(target: .NoticeDomain, type: .interface), + .domain(target: .UserDomain, type: .interface) + ]), + .tests(module: .feature(.DetailNoticeFeature), dependencies: [ + .feature(target: .DetailNoticeFeature), + .domain(target: .NoticeDomain, type: .testing), + .domain(target: .UserDomain, type: .testing) + ]), + .demo(module: .feature(.DetailNoticeFeature), dependencies: [ + .feature(target: .DetailNoticeFeature), + .domain(target: .NoticeDomain, type: .testing), + .domain(target: .UserDomain, type: .testing) + ]) + ] +) diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift new file mode 100644 index 00000000..c545ee60 --- /dev/null +++ b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift @@ -0,0 +1,33 @@ +import BaseFeature +import Combine +import Moordinator +import Store + +final class DetailNoticeStore: BaseStore { + var route: PassthroughSubject = .init() + var subscription: Set = .init() + var initialState: State + var stateSubject: CurrentValueSubject + + init() { + self.initialState = .init() + self.stateSubject = .init(initialState) + } + + struct State {} + enum Action {} + enum Mutation {} +} + +extension DetailNoticeStore { + func mutate(state: State, action: Action) -> SideEffect { + .none + } +} + +extension DetailNoticeStore { + func reduce(state: State, mutate: Mutation) -> State { + var newState = state + return newState + } +} diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift new file mode 100644 index 00000000..486a71c6 --- /dev/null +++ b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift @@ -0,0 +1,5 @@ +import BaseFeature +import UIKit + +final class DetailNoticeViewController: BaseStoredViewController { +} diff --git a/Projects/Feature/DetailNoticeFeature/Tests/DetailNoticeFeatureTest.swift b/Projects/Feature/DetailNoticeFeature/Tests/DetailNoticeFeatureTest.swift new file mode 100644 index 00000000..385b2dc6 --- /dev/null +++ b/Projects/Feature/DetailNoticeFeature/Tests/DetailNoticeFeatureTest.swift @@ -0,0 +1,11 @@ +import XCTest + +final class DetailNoticeFeatureTests: XCTestCase { + override func setUpWithError() throws {} + + override func tearDownWithError() throws {} + + func testExample() { + XCTAssertEqual(1, 1) + } +} diff --git a/Projects/Feature/NoticeFeature/Project.swift b/Projects/Feature/NoticeFeature/Project.swift index b9820202..0adc7d8d 100644 --- a/Projects/Feature/NoticeFeature/Project.swift +++ b/Projects/Feature/NoticeFeature/Project.swift @@ -7,6 +7,7 @@ let project = Project.module( targets: [ .implements(module: .feature(.NoticeFeature), dependencies: [ .feature(target: .BaseFeature), + .feature(target: .DetailNoticeFeature), .domain(target: .NoticeDomain, type: .interface), .domain(target: .UserDomain, type: .interface) ]), From 8c2322d094f2e190acd11a3eaf9ca2f12bc36dc3 Mon Sep 17 00:00:00 2001 From: baegteun Date: Fri, 28 Jul 2023 23:29:57 +0900 Subject: [PATCH 02/14] =?UTF-8?q?:lipstick:=20::=20[#122]=20DetailNoticeFe?= =?UTF-8?q?ature=20/=20=EA=B3=B5=EC=A7=80=20=EC=83=81=EC=84=B8=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Demo/Sources/AppDelegate.swift | 6 +- .../Scene/DetailNoticeViewController.swift | 62 +++++++++++++++++++ .../Sources/Label/DotoriLabel.swift | 15 +++++ 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift index ef75667f..3eb4c0c4 100644 --- a/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift @@ -12,10 +12,10 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { ) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let store = DetailNoticeStore() - let viewController = DetailNoticeViewController(store: store) - window?.rootViewController = Inject.ViewControllerHost( - UINavigationController(rootViewController: viewController) + let viewController = Inject.ViewControllerHost( + UINavigationController(rootViewController: DetailNoticeViewController(store: store)) ) + window?.rootViewController = viewController window?.makeKeyAndVisible() return true diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift index 486a71c6..0fe148ec 100644 --- a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift +++ b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift @@ -1,5 +1,67 @@ import BaseFeature +import DesignSystem +import MSGLayout import UIKit +import UIKitUtil final class DetailNoticeViewController: BaseStoredViewController { + private enum Metric { + static let horizontalPadding: CGFloat = 20 + } + private let rewriteBarButton = UIBarButtonItem( + image: .Dotori.pen.tintColor(color: .dotori(.neutral(.n10))) + ) + private let removeBarButton = UIBarButtonItem( + image: .Dotori.trashcan.tintColor(color: .dotori(.sub(.red))) + ) + private let signatureColorView = UIView() + .set(\.cornerRadius, 6) + .set(\.clipsToBounds, true) + private let authorLabel = DotoriLabel(font: .h4) + private let dateLabel = DotoriLabel(textColor: .neutral(.n20), font: .body2) + private let titleLabel = DotoriLabel(textColor: .neutral(.n10), font: .h3) + private lazy var headerStackView = VStackView(spacing: 8) { + HStackView(spacing: 8) { + signatureColorView + .width(12) + .height(12) + + authorLabel + + SpacerView() + + dateLabel + } + .alignment(.center) + + titleLabel + } + private let contentLabel = DotoriLabel("", font: .body1) + .set(\.numberOfLines, 0) + .set(\.clipsToBounds, true) + private lazy var contentStackView = VStackView(spacing: 16) { + contentLabel + } + .margin(.axes(vertical: 12, horizontal: 16)) + .set(\.backgroundColor, .dotori(.background(.bg))) + .set(\.cornerRadius, 8) + + override func setLayout() { + MSGLayout.stackedScrollLayout(self.view) { + VStackView(spacing: 24) { + headerStackView + + contentStackView + } + .margin(.axes(vertical: 16, horizontal: 20)) + } + } + + override func configureViewController() { + self.view.backgroundColor = .dotori(.background(.card)) + } + + override func configureNavigation() { + self.navigationItem.setRightBarButtonItems([removeBarButton, rewriteBarButton], animated: true) + } } diff --git a/Projects/UserInterface/DesignSystem/Sources/Label/DotoriLabel.swift b/Projects/UserInterface/DesignSystem/Sources/Label/DotoriLabel.swift index 7a217039..17b950e4 100644 --- a/Projects/UserInterface/DesignSystem/Sources/Label/DotoriLabel.swift +++ b/Projects/UserInterface/DesignSystem/Sources/Label/DotoriLabel.swift @@ -1,6 +1,17 @@ +import MSGLayout import UIKit public final class DotoriLabel: UILabel { + public var padding = UIEdgeInsets.all(0) + + public override var intrinsicContentSize: CGSize { + var contentSize = super.intrinsicContentSize + contentSize.height += padding.top + padding.bottom + contentSize.width += padding.left + padding.right + + return contentSize + } + public init( _ title: String = "", textColor: UIColor.DotoriColorSystem = .neutral(.n10), @@ -12,6 +23,10 @@ public final class DotoriLabel: UILabel { self.textColor = .dotori(textColor) } + public override func drawText(in rect: CGRect) { + super.drawText(in: rect.inset(by: padding)) + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } From 1c533d5c89db022e41f1244fd503e5985da0fc94 Mon Sep 17 00:00:00 2001 From: baegteun Date: Fri, 28 Jul 2023 23:48:54 +0900 Subject: [PATCH 03/14] :sparkles: :: [#122] NoticeDomain / FetchNoticeUseCase Implementation --- .../DataSource/RemoteNoticeDataSource.swift | 1 + .../Interface/Entity/DetailNoticeEntity.swift | 40 +++++++++++++++++ .../Interface/Model/DetailNoticeModel.swift | 1 + .../Repository/NoticeRepository.swift | 1 + .../UseCase/FetchNoticeUseCase.swift | 3 ++ .../Assembly/NoticeDomainAssembly.swift | 4 ++ .../DTO/Response/FetchNoticeResponseDTO.swift | 44 +++++++++++++++++++ .../Sources/DataSource/NoticeEndpoint.swift | 4 ++ .../RemoteNoticeDataSourceImpl.swift | 8 ++++ .../Repository/NoticeRepositoryImpl.swift | 4 ++ .../UseCase/FetchNoticeUseCaseImpl.swift | 13 ++++++ .../RemoteNoticeDataSourceSpy.swift | 17 +++++++ .../Repository/NoticeRepositorySpy.swift | 16 +++++++ .../UseCase/FetchNoticeUseCaseSpy.swift | 19 ++++++++ 14 files changed, 175 insertions(+) create mode 100644 Projects/Domain/NoticeDomain/Interface/Entity/DetailNoticeEntity.swift create mode 100644 Projects/Domain/NoticeDomain/Interface/Model/DetailNoticeModel.swift create mode 100644 Projects/Domain/NoticeDomain/Interface/UseCase/FetchNoticeUseCase.swift create mode 100644 Projects/Domain/NoticeDomain/Sources/DTO/Response/FetchNoticeResponseDTO.swift create mode 100644 Projects/Domain/NoticeDomain/Sources/UseCase/FetchNoticeUseCaseImpl.swift create mode 100644 Projects/Domain/NoticeDomain/Testing/UseCase/FetchNoticeUseCaseSpy.swift diff --git a/Projects/Domain/NoticeDomain/Interface/DataSource/RemoteNoticeDataSource.swift b/Projects/Domain/NoticeDomain/Interface/DataSource/RemoteNoticeDataSource.swift index 117dbf51..75cbfed1 100644 --- a/Projects/Domain/NoticeDomain/Interface/DataSource/RemoteNoticeDataSource.swift +++ b/Projects/Domain/NoticeDomain/Interface/DataSource/RemoteNoticeDataSource.swift @@ -1,3 +1,4 @@ public protocol RemoteNoticeDataSource { func fetchNoticeList() async throws -> [NoticeEntity] + func fetchNotice(id: Int) async throws -> DetailNoticeEntity } diff --git a/Projects/Domain/NoticeDomain/Interface/Entity/DetailNoticeEntity.swift b/Projects/Domain/NoticeDomain/Interface/Entity/DetailNoticeEntity.swift new file mode 100644 index 00000000..31f352a9 --- /dev/null +++ b/Projects/Domain/NoticeDomain/Interface/Entity/DetailNoticeEntity.swift @@ -0,0 +1,40 @@ +import BaseDomainInterface +import Foundation + +public struct DetailNoticeEntity: Equatable { + public let id: Int + public let title: String + public let content: String + public let role: UserRoleType + public let images: [NoticeImage] + public let createdDate: Date + public let modifiedDate: Date? + + public init( + id: Int, + title: String, + content: String, + role: UserRoleType, + images: [DetailNoticeEntity.NoticeImage], + createdDate: Date, + modifiedDate: Date? = nil + ) { + self.id = id + self.title = title + self.content = content + self.role = role + self.images = images + self.createdDate = createdDate + self.modifiedDate = modifiedDate + } + + public struct NoticeImage: Equatable { + public let id: Int + public let imageURL: String + + public init(id: Int, imageURL: String) { + self.id = id + self.imageURL = imageURL + } + } +} diff --git a/Projects/Domain/NoticeDomain/Interface/Model/DetailNoticeModel.swift b/Projects/Domain/NoticeDomain/Interface/Model/DetailNoticeModel.swift new file mode 100644 index 00000000..2774090c --- /dev/null +++ b/Projects/Domain/NoticeDomain/Interface/Model/DetailNoticeModel.swift @@ -0,0 +1 @@ +public typealias DetailNoticeModel = DetailNoticeEntity diff --git a/Projects/Domain/NoticeDomain/Interface/Repository/NoticeRepository.swift b/Projects/Domain/NoticeDomain/Interface/Repository/NoticeRepository.swift index bcdd8375..d74aaffc 100644 --- a/Projects/Domain/NoticeDomain/Interface/Repository/NoticeRepository.swift +++ b/Projects/Domain/NoticeDomain/Interface/Repository/NoticeRepository.swift @@ -1,3 +1,4 @@ public protocol NoticeRepository { func fetchNoticeList() async throws -> [NoticeEntity] + func fetchNotice(id: Int) async throws -> DetailNoticeEntity } diff --git a/Projects/Domain/NoticeDomain/Interface/UseCase/FetchNoticeUseCase.swift b/Projects/Domain/NoticeDomain/Interface/UseCase/FetchNoticeUseCase.swift new file mode 100644 index 00000000..d6f73516 --- /dev/null +++ b/Projects/Domain/NoticeDomain/Interface/UseCase/FetchNoticeUseCase.swift @@ -0,0 +1,3 @@ +public protocol FetchNoticeUseCase { + func callAsFunction(id: Int) async throws -> DetailNoticeModel +} diff --git a/Projects/Domain/NoticeDomain/Sources/Assembly/NoticeDomainAssembly.swift b/Projects/Domain/NoticeDomain/Sources/Assembly/NoticeDomainAssembly.swift index 101afd63..8954fdc8 100644 --- a/Projects/Domain/NoticeDomain/Sources/Assembly/NoticeDomainAssembly.swift +++ b/Projects/Domain/NoticeDomain/Sources/Assembly/NoticeDomainAssembly.swift @@ -18,5 +18,9 @@ public final class NoticeDomainAssembly: Assembly { container.register(FetchNoticeListUseCase.self) { resolver in FetchNoticeListUseCaseImpl(noticeRepository: resolver.resolve(NoticeRepository.self)!) } + + container.register(FetchNoticeUseCase.self) { resolver in + FetchNoticeUseCaseImpl(noticeRepository: resolver.resolve(NoticeRepository.self)!) + } } } diff --git a/Projects/Domain/NoticeDomain/Sources/DTO/Response/FetchNoticeResponseDTO.swift b/Projects/Domain/NoticeDomain/Sources/DTO/Response/FetchNoticeResponseDTO.swift new file mode 100644 index 00000000..c4a2d396 --- /dev/null +++ b/Projects/Domain/NoticeDomain/Sources/DTO/Response/FetchNoticeResponseDTO.swift @@ -0,0 +1,44 @@ +import BaseDomainInterface +import DateUtility +import Foundation +import NoticeDomainInterface + +struct FetchNoticeResponseDTO: Decodable { + let id: Int + let title: String + let content: String + let role: UserRoleType + let boardImage: [BoardImage] + let createdDate: String + let modifiedDate: String? + + struct BoardImage: Decodable { + let id: Int + let url: String + + init(id: Int, url: String) { + self.id = id + self.url = url + } + } +} + +extension FetchNoticeResponseDTO { + func toDomain() -> DetailNoticeEntity { + DetailNoticeEntity( + id: id, + title: title, + content: content, + role: role, + images: boardImage.map { $0.toDomain() }, + createdDate: createdDate.toDateWithCustomFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"), + modifiedDate: modifiedDate?.toDateWithCustomFormat("yyyy-MM-dd'T'HH:mm:ss.SSS") + ) + } +} + +extension FetchNoticeResponseDTO.BoardImage { + func toDomain() -> DetailNoticeEntity.NoticeImage { + .init(id: id, imageURL: url) + } +} diff --git a/Projects/Domain/NoticeDomain/Sources/DataSource/NoticeEndpoint.swift b/Projects/Domain/NoticeDomain/Sources/DataSource/NoticeEndpoint.swift index 7b26893b..514ad505 100644 --- a/Projects/Domain/NoticeDomain/Sources/DataSource/NoticeEndpoint.swift +++ b/Projects/Domain/NoticeDomain/Sources/DataSource/NoticeEndpoint.swift @@ -3,6 +3,7 @@ import NetworkingInterface enum NoticeEndpoint { case fetchNoticeList + case fetchNotice(id: Int) } extension NoticeEndpoint: DotoriEndpoint { @@ -14,6 +15,9 @@ extension NoticeEndpoint: DotoriEndpoint { switch self { case .fetchNoticeList: return .get("") + + case let .fetchNotice(id): + return .get("/\(id)") } } diff --git a/Projects/Domain/NoticeDomain/Sources/DataSource/RemoteNoticeDataSourceImpl.swift b/Projects/Domain/NoticeDomain/Sources/DataSource/RemoteNoticeDataSourceImpl.swift index 50c1c226..e461b2b8 100644 --- a/Projects/Domain/NoticeDomain/Sources/DataSource/RemoteNoticeDataSourceImpl.swift +++ b/Projects/Domain/NoticeDomain/Sources/DataSource/RemoteNoticeDataSourceImpl.swift @@ -15,4 +15,12 @@ final class RemoteNoticeDataSourceImpl: RemoteNoticeDataSource { ) .toDomain() } + + func fetchNotice(id: Int) async throws -> DetailNoticeEntity { + try await networking.request( + NoticeEndpoint.fetchNotice(id: id), + dto: FetchNoticeResponseDTO.self + ) + .toDomain() + } } diff --git a/Projects/Domain/NoticeDomain/Sources/Repository/NoticeRepositoryImpl.swift b/Projects/Domain/NoticeDomain/Sources/Repository/NoticeRepositoryImpl.swift index 75e4b7e6..eaf51f98 100644 --- a/Projects/Domain/NoticeDomain/Sources/Repository/NoticeRepositoryImpl.swift +++ b/Projects/Domain/NoticeDomain/Sources/Repository/NoticeRepositoryImpl.swift @@ -10,4 +10,8 @@ final class NoticeRepositoryImpl: NoticeRepository { func fetchNoticeList() async throws -> [NoticeEntity] { try await remoteNoticeDataSource.fetchNoticeList() } + + func fetchNotice(id: Int) async throws -> DetailNoticeEntity { + try await remoteNoticeDataSource.fetchNotice(id: id) + } } diff --git a/Projects/Domain/NoticeDomain/Sources/UseCase/FetchNoticeUseCaseImpl.swift b/Projects/Domain/NoticeDomain/Sources/UseCase/FetchNoticeUseCaseImpl.swift new file mode 100644 index 00000000..1788707e --- /dev/null +++ b/Projects/Domain/NoticeDomain/Sources/UseCase/FetchNoticeUseCaseImpl.swift @@ -0,0 +1,13 @@ +import NoticeDomainInterface + +struct FetchNoticeUseCaseImpl: FetchNoticeUseCase { + private let noticeRepository: any NoticeRepository + + init(noticeRepository: any NoticeRepository) { + self.noticeRepository = noticeRepository + } + + func callAsFunction(id: Int) async throws -> DetailNoticeModel { + try await noticeRepository.fetchNotice(id: id) + } +} diff --git a/Projects/Domain/NoticeDomain/Testing/DataSource/RemoteNoticeDataSourceSpy.swift b/Projects/Domain/NoticeDomain/Testing/DataSource/RemoteNoticeDataSourceSpy.swift index 3a7ce198..d1582b9e 100644 --- a/Projects/Domain/NoticeDomain/Testing/DataSource/RemoteNoticeDataSourceSpy.swift +++ b/Projects/Domain/NoticeDomain/Testing/DataSource/RemoteNoticeDataSourceSpy.swift @@ -1,3 +1,4 @@ +import BaseDomainInterface import NoticeDomainInterface final class RemoteNoticeDataSourceSpy: RemoteNoticeDataSource { @@ -7,4 +8,20 @@ final class RemoteNoticeDataSourceSpy: RemoteNoticeDataSource { fetchNoticeListCallCount += 1 return fetchNoticeListReturn } + + var fetchNoticeCallCount = 0 + var fetchNoticeHandler: (Int) async throws -> DetailNoticeEntity = { _ in + .init( + id: 1, + title: "", + content: "", + role: .member, + images: [], + createdDate: .init() + ) + } + func fetchNotice(id: Int) async throws -> DetailNoticeEntity { + fetchNoticeCallCount += 1 + return try await fetchNoticeHandler(id) + } } diff --git a/Projects/Domain/NoticeDomain/Testing/Repository/NoticeRepositorySpy.swift b/Projects/Domain/NoticeDomain/Testing/Repository/NoticeRepositorySpy.swift index aab959ee..92789bda 100644 --- a/Projects/Domain/NoticeDomain/Testing/Repository/NoticeRepositorySpy.swift +++ b/Projects/Domain/NoticeDomain/Testing/Repository/NoticeRepositorySpy.swift @@ -7,4 +7,20 @@ final class NoticeRepositorySpy: NoticeRepository { fetchNoticeListCallCount += 1 return fetchNoticeListReturn } + + var fetchNoticeCallCount = 0 + var fetchNoticeHandler: (Int) async throws -> DetailNoticeEntity = { _ in + .init( + id: 1, + title: "", + content: "", + role: .member, + images: [], + createdDate: .init() + ) + } + func fetchNotice(id: Int) async throws -> DetailNoticeEntity { + fetchNoticeCallCount += 1 + return try await fetchNoticeHandler(id) + } } diff --git a/Projects/Domain/NoticeDomain/Testing/UseCase/FetchNoticeUseCaseSpy.swift b/Projects/Domain/NoticeDomain/Testing/UseCase/FetchNoticeUseCaseSpy.swift new file mode 100644 index 00000000..219ee923 --- /dev/null +++ b/Projects/Domain/NoticeDomain/Testing/UseCase/FetchNoticeUseCaseSpy.swift @@ -0,0 +1,19 @@ +import NoticeDomainInterface + +final class FetchNoticeUseCaseSpy: FetchNoticeUseCase { + var fetchNoticeCallCount = 0 + var fetchNoticeHandler: (Int) async throws -> DetailNoticeModel = { _ in + .init( + id: 1, + title: "", + content: "", + role: .member, + images: [], + createdDate: .init() + ) + } + func callAsFunction(id: Int) async throws -> DetailNoticeModel { + fetchNoticeCallCount += 1 + return try await fetchNoticeHandler(id) + } +} From 2c94613e2a2c8c7216dca9faa0eb9b1e01c4c042 Mon Sep 17 00:00:00 2001 From: baegteun Date: Sat, 29 Jul 2023 15:58:37 +0900 Subject: [PATCH 04/14] :sparkles: :: [#122] DetailNoticeFeature / fetchNoticeUseCase DI --- .../DetailNoticeFeature/Demo/Sources/AppDelegate.swift | 6 +++++- .../Sources/Scene/DetailNoticeStore.swift | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift index 3eb4c0c4..b870a4f8 100644 --- a/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift @@ -1,6 +1,7 @@ import Inject import UIKit @testable import DetailNoticeFeature +@testable import NoticeDomainTesting @main final class AppDelegate: UIResponder, UIApplicationDelegate { @@ -11,7 +12,10 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) - let store = DetailNoticeStore() + let fetchNoticeUseCase = FetchNoticeUseCaseSpy() + let store = DetailNoticeStore( + fetchNoticeUseCase: fetchNoticeUseCase + ) let viewController = Inject.ViewControllerHost( UINavigationController(rootViewController: DetailNoticeViewController(store: store)) ) diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift index c545ee60..af970df9 100644 --- a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift +++ b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift @@ -1,6 +1,7 @@ import BaseFeature import Combine import Moordinator +import NoticeDomainInterface import Store final class DetailNoticeStore: BaseStore { @@ -8,10 +9,14 @@ final class DetailNoticeStore: BaseStore { var subscription: Set = .init() var initialState: State var stateSubject: CurrentValueSubject + private let fetchNoticeUseCase: any FetchNoticeUseCase - init() { + init( + fetchNoticeUseCase: any FetchNoticeUseCase + ) { self.initialState = .init() self.stateSubject = .init(initialState) + self.fetchNoticeUseCase = fetchNoticeUseCase } struct State {} From a8a818caf8524e6f15143367cb7cc04b72e53a56 Mon Sep 17 00:00:00 2001 From: baegteun Date: Sat, 29 Jul 2023 18:38:35 +0900 Subject: [PATCH 05/14] =?UTF-8?q?:sparkles:=20::=20[#122]=20DetailNoticeFe?= =?UTF-8?q?ature=20/=20=EA=B3=B5=EC=A7=80=20=EC=83=81=EC=84=B8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=B0=94=EC=9D=B8=EB=94=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dependency+SPM.swift | 1 + .../App/Sources/Application/AppDelegate.swift | 2 + Projects/Feature/BaseFeature/Project.swift | 1 + .../Assembly/DetailNoticeAssembly.swift | 13 ++++ .../Sources/Factory/DetailNoticeFactory.swift | 6 ++ .../Factory/DetailNoticeFactoryImpl.swift | 19 +++++ .../Sources/Scene/DetailNoticeStore.swift | 59 ++++++++++++++- .../Scene/DetailNoticeViewController.swift | 74 +++++++++++++++++++ .../Tests/DetailNoticeFeatureTest.swift | 15 +++- .../Resources/en.lproj/Notice.strings | 3 +- .../Resources/ko.lproj/Notice.strings | 1 + Tuist/Dependencies.swift | 1 + 12 files changed, 186 insertions(+), 9 deletions(-) create mode 100644 Projects/Feature/DetailNoticeFeature/Sources/Assembly/DetailNoticeAssembly.swift create mode 100644 Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactory.swift create mode 100644 Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactoryImpl.swift diff --git a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+SPM.swift b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+SPM.swift index 02ee9725..b6c36216 100644 --- a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+SPM.swift +++ b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+SPM.swift @@ -5,6 +5,7 @@ public extension TargetDependency { } public extension TargetDependency.SPM { + static let Nuke = TargetDependency.external(name: "Nuke") static let Anim = TargetDependency.external(name: "Anim") static let CombineMiniature = TargetDependency.external(name: "CombineMiniature") static let AsyncNeiSwift = TargetDependency.external(name: "AsyncNeiSwift") diff --git a/Projects/App/Sources/Application/AppDelegate.swift b/Projects/App/Sources/Application/AppDelegate.swift index 3d74ce3f..e422079e 100644 --- a/Projects/App/Sources/Application/AppDelegate.swift +++ b/Projects/App/Sources/Application/AppDelegate.swift @@ -1,6 +1,7 @@ import AuthDomain import ConfirmationDialogFeature import Database +import DetailNoticeFeature import HomeFeature import IQKeyboardManagerSwift import JwtStore @@ -47,6 +48,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { HomeAssembly(), MyViolationListAssembly(), NoticeAssembly(), + DetailNoticeAssembly(), SelfStudyAssembly(), ConfirmationDialogAssembly(), MassageAssembly(), diff --git a/Projects/Feature/BaseFeature/Project.swift b/Projects/Feature/BaseFeature/Project.swift index 3c64319d..0a58efce 100644 --- a/Projects/Feature/BaseFeature/Project.swift +++ b/Projects/Feature/BaseFeature/Project.swift @@ -9,6 +9,7 @@ let project = Project.module( .SPM.Moordinator, .SPM.Store, .SPM.IQKeyboardManagerSwift, + .SPM.Nuke, .userInterface(target: .DesignSystem), .userInterface(target: .Localization), .shared(target: .GlobalThirdPartyLibrary), diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Assembly/DetailNoticeAssembly.swift b/Projects/Feature/DetailNoticeFeature/Sources/Assembly/DetailNoticeAssembly.swift new file mode 100644 index 00000000..2d455ec0 --- /dev/null +++ b/Projects/Feature/DetailNoticeFeature/Sources/Assembly/DetailNoticeAssembly.swift @@ -0,0 +1,13 @@ +import Swinject +import NoticeDomainInterface + +public final class DetailNoticeAssembly: Assembly { + public init() {} + public func assemble(container: Container) { + container.register(DetailNoticeFactory.self) { resolver in + DetailNoticeFactoryImpl( + fetchNoticeUseCase: resolver.resolve(FetchNoticeUseCase.self)! + ) + } + } +} diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactory.swift b/Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactory.swift new file mode 100644 index 00000000..02b16373 --- /dev/null +++ b/Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactory.swift @@ -0,0 +1,6 @@ +import BaseFeature +import UIKit + +public protocol DetailNoticeFactory { + func makeViewController(noticeID: Int) -> any StoredViewControllable +} diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactoryImpl.swift b/Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactoryImpl.swift new file mode 100644 index 00000000..316f1f17 --- /dev/null +++ b/Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactoryImpl.swift @@ -0,0 +1,19 @@ +import BaseFeature +import NoticeDomainInterface +import UIKit + +struct DetailNoticeFactoryImpl: DetailNoticeFactory { + private let fetchNoticeUseCase: any FetchNoticeUseCase + + init(fetchNoticeUseCase: any FetchNoticeUseCase) { + self.fetchNoticeUseCase = fetchNoticeUseCase + } + + func makeViewController(noticeID: Int) -> any StoredViewControllable { + let store = DetailNoticeStore( + noticeID: noticeID, + fetchNoticeUseCase: fetchNoticeUseCase + ) + return DetailNoticeViewController(store: store) + } +} diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift index af970df9..03d98bd4 100644 --- a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift +++ b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift @@ -9,30 +9,81 @@ final class DetailNoticeStore: BaseStore { var subscription: Set = .init() var initialState: State var stateSubject: CurrentValueSubject + private let noticeID: Int private let fetchNoticeUseCase: any FetchNoticeUseCase init( + noticeID: Int, fetchNoticeUseCase: any FetchNoticeUseCase ) { self.initialState = .init() self.stateSubject = .init(initialState) + self.noticeID = noticeID self.fetchNoticeUseCase = fetchNoticeUseCase } - struct State {} - enum Action {} - enum Mutation {} + struct State { + var detailNotice: DetailNoticeModel? + var isLoading = false + } + enum Action { + case viewWillAppear + } + enum Mutation { + case updateDetailNotice(DetailNoticeModel) + case updateIsLoading(Bool) + } } extension DetailNoticeStore { func mutate(state: State, action: Action) -> SideEffect { - .none + switch action { + case .viewWillAppear: + return self.viewWillAppear() + } + return .none } } extension DetailNoticeStore { func reduce(state: State, mutate: Mutation) -> State { var newState = state + switch mutate { + case let .updateDetailNotice(detailNotice): + newState.detailNotice = detailNotice + + case let .updateIsLoading(isLoading): + newState.isLoading = isLoading + } return newState } } + +// MARK: - Mutate +private extension DetailNoticeStore { + func viewWillAppear() -> SideEffect { + let detailNoticeEffect = SideEffect + .tryAsync { [noticeID, fetchNoticeUseCase] in + try await fetchNoticeUseCase(id: noticeID) + } + .catchToNever() + .map(Mutation.updateDetailNotice) + return self.makeLoadingSideEffect(detailNoticeEffect) + } +} + +// MARK: - Reusable +private extension DetailNoticeStore { + func makeLoadingSideEffect( + _ publisher: SideEffect + ) -> SideEffect { + let startLoadingPublisher = SideEffect + .just(Mutation.updateIsLoading(true)) + let endLoadingPublisher = SideEffect + .just(Mutation.updateIsLoading(false)) + return startLoadingPublisher + .append(publisher) + .append(endLoadingPublisher) + .eraseToSideEffect() + } +} diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift index 0fe148ec..131bf700 100644 --- a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift +++ b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift @@ -1,6 +1,12 @@ +import BaseDomainInterface import BaseFeature +import ConcurrencyUtil +import DateUtility import DesignSystem +import Localization import MSGLayout +import NoticeDomainInterface +import Nuke import UIKit import UIKitUtil @@ -64,4 +70,72 @@ final class DetailNoticeViewController: BaseStoredViewController Date: Sat, 29 Jul 2023 20:42:12 +0900 Subject: [PATCH 06/14] =?UTF-8?q?:sparkles:=20::=20[#122]=20NoticeFeature?= =?UTF-8?q?=20/=20DetailNotice=20=ED=99=94=EB=A9=B4=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BaseFeature/Sources/DotoriRoutePath.swift | 2 +- .../Sources/Assembly/NoticeAssembly.swift | 4 ++- .../Sources/Factory/NoticeFactoryImpl.swift | 11 ++++++-- .../Moordinator/NoticeMoordinator.swift | 26 ++++++++++++++++--- .../Sources/Scene/NoticeStore.swift | 23 +++++++++------- .../Sources/Scene/NoticeViewController.swift | 26 ++++++++++--------- 6 files changed, 64 insertions(+), 28 deletions(-) diff --git a/Projects/Feature/BaseFeature/Sources/DotoriRoutePath.swift b/Projects/Feature/BaseFeature/Sources/DotoriRoutePath.swift index fc1bbb2b..b7be254e 100644 --- a/Projects/Feature/BaseFeature/Sources/DotoriRoutePath.swift +++ b/Projects/Feature/BaseFeature/Sources/DotoriRoutePath.swift @@ -32,7 +32,7 @@ public enum DotoriRoutePath: RoutePath { // MARK: Notice case notice - case noticeDetail(noticeID: Int) + case detailNotice(noticeID: Int) // MARK: SelfStudy case selfStudy diff --git a/Projects/Feature/NoticeFeature/Sources/Assembly/NoticeAssembly.swift b/Projects/Feature/NoticeFeature/Sources/Assembly/NoticeAssembly.swift index 703a2068..2a2ad289 100644 --- a/Projects/Feature/NoticeFeature/Sources/Assembly/NoticeAssembly.swift +++ b/Projects/Feature/NoticeFeature/Sources/Assembly/NoticeAssembly.swift @@ -1,3 +1,4 @@ +import DetailNoticeFeature import NoticeDomainInterface import Swinject import UserDomainInterface @@ -8,7 +9,8 @@ public final class NoticeAssembly: Assembly { container.register(NoticeFactory.self) { resolver in NoticeFactoryImpl( fetchNoticeListUseCase: resolver.resolve(FetchNoticeListUseCase.self)!, - loadCurrentUserRoleUseCase: resolver.resolve(LoadCurrentUserRoleUseCase.self)! + loadCurrentUserRoleUseCase: resolver.resolve(LoadCurrentUserRoleUseCase.self)!, + detailNoticeFactory: resolver.resolve(DetailNoticeFactory.self)! ) } } diff --git a/Projects/Feature/NoticeFeature/Sources/Factory/NoticeFactoryImpl.swift b/Projects/Feature/NoticeFeature/Sources/Factory/NoticeFactoryImpl.swift index 7e15246c..c47f6371 100644 --- a/Projects/Feature/NoticeFeature/Sources/Factory/NoticeFactoryImpl.swift +++ b/Projects/Feature/NoticeFeature/Sources/Factory/NoticeFactoryImpl.swift @@ -1,3 +1,4 @@ +import DetailNoticeFeature import Moordinator import NoticeDomainInterface import UserDomainInterface @@ -5,13 +6,16 @@ import UserDomainInterface struct NoticeFactoryImpl: NoticeFactory { private let fetchNoticeListUseCase: any FetchNoticeListUseCase private let loadCurrentUserRoleUseCase: any LoadCurrentUserRoleUseCase + private let detailNoticeFactory: any DetailNoticeFactory init( fetchNoticeListUseCase: any FetchNoticeListUseCase, - loadCurrentUserRoleUseCase: any LoadCurrentUserRoleUseCase + loadCurrentUserRoleUseCase: any LoadCurrentUserRoleUseCase, + detailNoticeFactory: any DetailNoticeFactory ) { self.fetchNoticeListUseCase = fetchNoticeListUseCase self.loadCurrentUserRoleUseCase = loadCurrentUserRoleUseCase + self.detailNoticeFactory = detailNoticeFactory } func makeMoordinator() -> Moordinator { @@ -20,6 +24,9 @@ struct NoticeFactoryImpl: NoticeFactory { loadCurrentUserRoleUseCase: loadCurrentUserRoleUseCase ) let noticeViewController = NoticeViewController(store: noticeStore) - return NoticeMoordinator(noticeViewController: noticeViewController) + return NoticeMoordinator( + noticeViewController: noticeViewController, + detailNoticeFactory: detailNoticeFactory + ) } } diff --git a/Projects/Feature/NoticeFeature/Sources/Moordinator/NoticeMoordinator.swift b/Projects/Feature/NoticeFeature/Sources/Moordinator/NoticeMoordinator.swift index 56e0e95a..930c2b80 100644 --- a/Projects/Feature/NoticeFeature/Sources/Moordinator/NoticeMoordinator.swift +++ b/Projects/Feature/NoticeFeature/Sources/Moordinator/NoticeMoordinator.swift @@ -1,17 +1,23 @@ import BaseFeature import DWebKit -import UIKit +import DetailNoticeFeature import Moordinator +import UIKit final class NoticeMoordinator: Moordinator { private let rootVC = UINavigationController() private let noticeViewController: any StoredViewControllable + private let detailNoticeFactory: any DetailNoticeFactory var root: Presentable { rootVC } - init(noticeViewController: any StoredViewControllable) { + init( + noticeViewController: any StoredViewControllable, + detailNoticeFactory: any DetailNoticeFactory + ) { self.noticeViewController = noticeViewController + self.detailNoticeFactory = detailNoticeFactory } func route(to path: RoutePath) -> MoordinatorContributors { @@ -20,6 +26,9 @@ final class NoticeMoordinator: Moordinator { case .notice: return coordinateToNotice() + case let .detailNotice(noticeID): + return navigateToDetailNotice(noticeID: noticeID) + default: return .none } @@ -30,6 +39,17 @@ final class NoticeMoordinator: Moordinator { private extension NoticeMoordinator { func coordinateToNotice() -> MoordinatorContributors { self.rootVC.setViewControllers([self.noticeViewController], animated: true) - return .one(.contribute(withNextPresentable: self.noticeViewController, withNextRouter: self.noticeViewController.store)) + return .one( + .contribute( + withNextPresentable: self.noticeViewController, + withNextRouter: self.noticeViewController.store + ) + ) + } + + func navigateToDetailNotice(noticeID: Int) -> MoordinatorContributors { + let viewController = detailNoticeFactory.makeViewController(noticeID: noticeID) + self.rootVC.pushViewController(viewController, animated: true) + return .none } } diff --git a/Projects/Feature/NoticeFeature/Sources/Scene/NoticeStore.swift b/Projects/Feature/NoticeFeature/Sources/Scene/NoticeStore.swift index f643ced4..539850d2 100644 --- a/Projects/Feature/NoticeFeature/Sources/Scene/NoticeStore.swift +++ b/Projects/Feature/NoticeFeature/Sources/Scene/NoticeStore.swift @@ -41,7 +41,8 @@ final class NoticeStore: BaseStore { case viewDidLoad case fetchNoticeList case editButtonDidTap - case noticeDidTap(Int) + case noticeDidSelect(Int) + case noticeDidDeselect(Int) } enum Mutation { case updateNoticeList([NoticeModel]) @@ -69,8 +70,11 @@ extension NoticeStore { .just(.removeAllSelectedNotice) ) - case let .noticeDidTap(noticeID): - return noticeDidTap(noticeID: noticeID) + case let .noticeDidSelect(noticeID): + return noticeDidSelect(noticeID: noticeID) + + case let .noticeDidDeselect(noticeID): + return noticeDidDeselect(noticeID: noticeID) } return .none } @@ -129,15 +133,16 @@ private extension NoticeStore { .eraseToSideEffect() } - func noticeDidTap(noticeID: Int) -> SideEffect { + func noticeDidSelect(noticeID: Int) -> SideEffect { guard currentState.isEditingMode else { - route.send(DotoriRoutePath.noticeDetail(noticeID: noticeID)) + route.send(DotoriRoutePath.detailNotice(noticeID: noticeID)) return .none } - let mutation = currentState.selectedNotice.contains(noticeID) - ? Mutation.removeSelectedNotice(noticeID) - : Mutation.insertSelectedNotice(noticeID) - return .just(mutation) + return .just(.insertSelectedNotice(noticeID)) + } + + func noticeDidDeselect(noticeID: Int) -> SideEffect { + return .just(.removeSelectedNotice(noticeID)) } } diff --git a/Projects/Feature/NoticeFeature/Sources/Scene/NoticeViewController.swift b/Projects/Feature/NoticeFeature/Sources/Scene/NoticeViewController.swift index cf89d0f4..ad851035 100644 --- a/Projects/Feature/NoticeFeature/Sources/Scene/NoticeViewController.swift +++ b/Projects/Feature/NoticeFeature/Sources/Scene/NoticeViewController.swift @@ -32,7 +32,7 @@ final class NoticeViewController: BaseStoredViewController { .set(\.backgroundColor, .clear) .set(\.separatorStyle, .none) .set(\.sectionHeaderTopPadding, 0) - .set(\.allowsSelection, false) + .set(\.sectionHeaderHeight, 0) .set(\.allowsMultipleSelection, false) .then { $0.register(cellType: NoticeCell.self) @@ -98,9 +98,14 @@ final class NoticeViewController: BaseStoredViewController { .store(in: &subscription) noticeTableAdapter.modelSelected - .merge(with: noticeTableAdapter.modelDeselected) .map(\.id) - .map(Store.Action.noticeDidTap) + .map(Store.Action.noticeDidSelect) + .sink(receiveValue: store.send(_:)) + .store(in: &subscription) + + noticeTableAdapter.modelDeselected + .map(\.id) + .map(Store.Action.noticeDidDeselect) .sink(receiveValue: store.send(_:)) .store(in: &subscription) @@ -110,6 +115,7 @@ final class NoticeViewController: BaseStoredViewController { .store(in: &subscription) } + // swiftlint: disable function_body_length override func bindState() { let sharedState = store.state.share() .receive(on: DispatchQueue.main) @@ -132,7 +138,6 @@ final class NoticeViewController: BaseStoredViewController { .map { $0.currentUserRole != .member && $0.isEditingMode } .removeDuplicates() .sink(with: noticeTableView, receiveValue: { tableView, isEditing in - tableView.allowsSelection = isEditing tableView.allowsMultipleSelection = isEditing tableView.reloadData() }) @@ -141,24 +146,20 @@ final class NoticeViewController: BaseStoredViewController { sharedState .map(\.currentUserRole) .map { $0 == .member } + .removeDuplicates() .sink { [editButton, writeOrRemoveButton] isMember in editButton.isHidden = isMember writeOrRemoveButton.isHidden = isMember } .store(in: &subscription) - sharedState - .map(\.isEditingMode) - .map { $0 ? L10n.Global.cancelButtonTitle : L10n.Notice.editButtonTitle } - .sink(with: editButton, receiveValue: { editButton, title in - editButton.setTitle(title, for: .normal) - }) - .store(in: &subscription) - sharedState .map(\.isEditingMode) .sink(receiveValue: { [weak self] isEditingMode in self?.transformWriteOrRemoveButton(isEditMode: isEditingMode) + + let title = isEditingMode ? L10n.Global.cancelButtonTitle : L10n.Notice.editButtonTitle + self?.editButton.setTitle(title, for: .normal) }) .store(in: &subscription) @@ -171,6 +172,7 @@ final class NoticeViewController: BaseStoredViewController { }) .store(in: &subscription) } + // swiftlint: enable function_body_length } private extension NoticeViewController { From 80b747349d1309e9c78a4756bb3a4fad9bbf52c6 Mon Sep 17 00:00:00 2001 From: baegteun Date: Sat, 29 Jul 2023 21:06:59 +0900 Subject: [PATCH 07/14] =?UTF-8?q?:sparkles:=20::=20[#122]=20NoticeDomain?= =?UTF-8?q?=20/=20=EA=B3=B5=EC=A7=80=20=EC=82=AD=EC=A0=9C=20UseCase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DataSource/RemoteNoticeDataSource.swift | 1 + .../Repository/NoticeRepository.swift | 1 + .../UseCase/RemoveNoticeUseCase.swift | 3 ++ .../Sources/DataSource/NoticeEndpoint.swift | 4 ++ .../RemoteNoticeDataSourceImpl.swift | 4 ++ .../Repository/NoticeRepositoryImpl.swift | 4 ++ .../UseCase/RemoveNoticeUseCaseImpl.swift | 13 +++++++ .../RemoteNoticeDataSourceSpy.swift | 7 ++++ .../Repository/NoticeRepositorySpy.swift | 7 ++++ .../UseCase/RemoveNoticeUseCaseSpy.swift | 10 +++++ .../Tests/FetchNoticeUseCaseTests.swift | 39 +++++++++++++++++++ .../Tests/NoticeRepositoryTests.swift | 22 ++++++++++- .../Tests/RemoveNoticeUseCaseTests.swift | 27 +++++++++++++ .../Assembly/DetailNoticeAssembly.swift | 6 ++- .../Factory/DetailNoticeFactoryImpl.swift | 11 +++++- .../Sources/Scene/DetailNoticeStore.swift | 25 +++++++++++- .../Scene/DetailNoticeViewController.swift | 28 +++++++++---- .../Tests/DetailNoticeFeatureTest.swift | 10 ++++- .../Sources/Scene/NoticeViewController.swift | 2 +- 19 files changed, 208 insertions(+), 16 deletions(-) create mode 100644 Projects/Domain/NoticeDomain/Interface/UseCase/RemoveNoticeUseCase.swift create mode 100644 Projects/Domain/NoticeDomain/Sources/UseCase/RemoveNoticeUseCaseImpl.swift create mode 100644 Projects/Domain/NoticeDomain/Testing/UseCase/RemoveNoticeUseCaseSpy.swift create mode 100644 Projects/Domain/NoticeDomain/Tests/FetchNoticeUseCaseTests.swift create mode 100644 Projects/Domain/NoticeDomain/Tests/RemoveNoticeUseCaseTests.swift diff --git a/Projects/Domain/NoticeDomain/Interface/DataSource/RemoteNoticeDataSource.swift b/Projects/Domain/NoticeDomain/Interface/DataSource/RemoteNoticeDataSource.swift index 75cbfed1..6f55e8f3 100644 --- a/Projects/Domain/NoticeDomain/Interface/DataSource/RemoteNoticeDataSource.swift +++ b/Projects/Domain/NoticeDomain/Interface/DataSource/RemoteNoticeDataSource.swift @@ -1,4 +1,5 @@ public protocol RemoteNoticeDataSource { func fetchNoticeList() async throws -> [NoticeEntity] func fetchNotice(id: Int) async throws -> DetailNoticeEntity + func removeNotice(id: Int) async throws } diff --git a/Projects/Domain/NoticeDomain/Interface/Repository/NoticeRepository.swift b/Projects/Domain/NoticeDomain/Interface/Repository/NoticeRepository.swift index d74aaffc..cc68009b 100644 --- a/Projects/Domain/NoticeDomain/Interface/Repository/NoticeRepository.swift +++ b/Projects/Domain/NoticeDomain/Interface/Repository/NoticeRepository.swift @@ -1,4 +1,5 @@ public protocol NoticeRepository { func fetchNoticeList() async throws -> [NoticeEntity] func fetchNotice(id: Int) async throws -> DetailNoticeEntity + func removeNotice(id: Int) async throws } diff --git a/Projects/Domain/NoticeDomain/Interface/UseCase/RemoveNoticeUseCase.swift b/Projects/Domain/NoticeDomain/Interface/UseCase/RemoveNoticeUseCase.swift new file mode 100644 index 00000000..c4b3e867 --- /dev/null +++ b/Projects/Domain/NoticeDomain/Interface/UseCase/RemoveNoticeUseCase.swift @@ -0,0 +1,3 @@ +public protocol RemoveNoticeUseCase { + func callAsFunction(id: Int) async throws +} diff --git a/Projects/Domain/NoticeDomain/Sources/DataSource/NoticeEndpoint.swift b/Projects/Domain/NoticeDomain/Sources/DataSource/NoticeEndpoint.swift index 514ad505..ac5dfc10 100644 --- a/Projects/Domain/NoticeDomain/Sources/DataSource/NoticeEndpoint.swift +++ b/Projects/Domain/NoticeDomain/Sources/DataSource/NoticeEndpoint.swift @@ -4,6 +4,7 @@ import NetworkingInterface enum NoticeEndpoint { case fetchNoticeList case fetchNotice(id: Int) + case removeNotice(id: Int) } extension NoticeEndpoint: DotoriEndpoint { @@ -18,6 +19,9 @@ extension NoticeEndpoint: DotoriEndpoint { case let .fetchNotice(id): return .get("/\(id)") + + case let .removeNotice(id): + return .delete("/\(id)") } } diff --git a/Projects/Domain/NoticeDomain/Sources/DataSource/RemoteNoticeDataSourceImpl.swift b/Projects/Domain/NoticeDomain/Sources/DataSource/RemoteNoticeDataSourceImpl.swift index e461b2b8..2b824561 100644 --- a/Projects/Domain/NoticeDomain/Sources/DataSource/RemoteNoticeDataSourceImpl.swift +++ b/Projects/Domain/NoticeDomain/Sources/DataSource/RemoteNoticeDataSourceImpl.swift @@ -23,4 +23,8 @@ final class RemoteNoticeDataSourceImpl: RemoteNoticeDataSource { ) .toDomain() } + + func removeNotice(id: Int) async throws { + try await networking.request(NoticeEndpoint.removeNotice(id: id)) + } } diff --git a/Projects/Domain/NoticeDomain/Sources/Repository/NoticeRepositoryImpl.swift b/Projects/Domain/NoticeDomain/Sources/Repository/NoticeRepositoryImpl.swift index eaf51f98..d36785d9 100644 --- a/Projects/Domain/NoticeDomain/Sources/Repository/NoticeRepositoryImpl.swift +++ b/Projects/Domain/NoticeDomain/Sources/Repository/NoticeRepositoryImpl.swift @@ -14,4 +14,8 @@ final class NoticeRepositoryImpl: NoticeRepository { func fetchNotice(id: Int) async throws -> DetailNoticeEntity { try await remoteNoticeDataSource.fetchNotice(id: id) } + + func removeNotice(id: Int) async throws { + try await remoteNoticeDataSource.removeNotice(id: id) + } } diff --git a/Projects/Domain/NoticeDomain/Sources/UseCase/RemoveNoticeUseCaseImpl.swift b/Projects/Domain/NoticeDomain/Sources/UseCase/RemoveNoticeUseCaseImpl.swift new file mode 100644 index 00000000..489cdce4 --- /dev/null +++ b/Projects/Domain/NoticeDomain/Sources/UseCase/RemoveNoticeUseCaseImpl.swift @@ -0,0 +1,13 @@ +import NoticeDomainInterface + +struct RemoveNoticeUseCaseImpl: RemoveNoticeUseCase { + private let noticeRepository: any NoticeRepository + + init(noticeRepository: any NoticeRepository) { + self.noticeRepository = noticeRepository + } + + func callAsFunction(id: Int) async throws { + try await noticeRepository.removeNotice(id: id) + } +} diff --git a/Projects/Domain/NoticeDomain/Testing/DataSource/RemoteNoticeDataSourceSpy.swift b/Projects/Domain/NoticeDomain/Testing/DataSource/RemoteNoticeDataSourceSpy.swift index d1582b9e..db3e91a4 100644 --- a/Projects/Domain/NoticeDomain/Testing/DataSource/RemoteNoticeDataSourceSpy.swift +++ b/Projects/Domain/NoticeDomain/Testing/DataSource/RemoteNoticeDataSourceSpy.swift @@ -24,4 +24,11 @@ final class RemoteNoticeDataSourceSpy: RemoteNoticeDataSource { fetchNoticeCallCount += 1 return try await fetchNoticeHandler(id) } + + var removeNoticeCallCount = 0 + var removeNoticeHandler: (Int) async throws -> Void = { _ in } + func removeNotice(id: Int) async throws { + removeNoticeCallCount += 1 + try await removeNoticeHandler(id) + } } diff --git a/Projects/Domain/NoticeDomain/Testing/Repository/NoticeRepositorySpy.swift b/Projects/Domain/NoticeDomain/Testing/Repository/NoticeRepositorySpy.swift index 92789bda..8ca120d5 100644 --- a/Projects/Domain/NoticeDomain/Testing/Repository/NoticeRepositorySpy.swift +++ b/Projects/Domain/NoticeDomain/Testing/Repository/NoticeRepositorySpy.swift @@ -23,4 +23,11 @@ final class NoticeRepositorySpy: NoticeRepository { fetchNoticeCallCount += 1 return try await fetchNoticeHandler(id) } + + var removeNoticeCallCount = 0 + var removeNoticeHandler: (Int) async throws -> Void = { _ in } + func removeNotice(id: Int) async throws { + removeNoticeCallCount += 1 + try await removeNoticeHandler(id) + } } diff --git a/Projects/Domain/NoticeDomain/Testing/UseCase/RemoveNoticeUseCaseSpy.swift b/Projects/Domain/NoticeDomain/Testing/UseCase/RemoveNoticeUseCaseSpy.swift new file mode 100644 index 00000000..29ea9797 --- /dev/null +++ b/Projects/Domain/NoticeDomain/Testing/UseCase/RemoveNoticeUseCaseSpy.swift @@ -0,0 +1,10 @@ +import NoticeDomainInterface + +final class RemoveNoticeUseCaseSpy: RemoveNoticeUseCase { + var removeNoticeCallCount = 0 + var removeNoticeHandler: (Int) async throws -> Void = { _ in } + func callAsFunction(id: Int) async throws { + removeNoticeCallCount += 1 + try await removeNoticeHandler(id) + } +} diff --git a/Projects/Domain/NoticeDomain/Tests/FetchNoticeUseCaseTests.swift b/Projects/Domain/NoticeDomain/Tests/FetchNoticeUseCaseTests.swift new file mode 100644 index 00000000..b91200bc --- /dev/null +++ b/Projects/Domain/NoticeDomain/Tests/FetchNoticeUseCaseTests.swift @@ -0,0 +1,39 @@ +import NoticeDomainInterface +import XCTest +@testable import NoticeDomain +@testable import NoticeDomainTesting + +final class FetchNoticeUseCaseTests: XCTestCase { + var noticeRepository: NoticeRepositorySpy! + var sut: FetchNoticeUseCaseImpl! + + override func setUp() { + noticeRepository = .init() + sut = .init(noticeRepository: noticeRepository) + } + + override func tearDown() { + noticeRepository = nil + sut = nil + } + + func testCallAsFunction() async throws { + let date = Date() + XCTAssertEqual(noticeRepository.fetchNoticeCallCount, 0) + let expected = DetailNoticeEntity( + id: 1, + title: "b", + content: "aj", + role: .councillor, + images: [], + createdDate: .init(), + modifiedDate: nil + ) + noticeRepository.fetchNoticeHandler = { _ in expected } + + let actual = try await sut(id: 1) + + XCTAssertEqual(noticeRepository.fetchNoticeCallCount, 1) + XCTAssertEqual(actual, expected) + } +} diff --git a/Projects/Domain/NoticeDomain/Tests/NoticeRepositoryTests.swift b/Projects/Domain/NoticeDomain/Tests/NoticeRepositoryTests.swift index 08e808ff..aabe7128 100644 --- a/Projects/Domain/NoticeDomain/Tests/NoticeRepositoryTests.swift +++ b/Projects/Domain/NoticeDomain/Tests/NoticeRepositoryTests.swift @@ -21,7 +21,7 @@ final class NoticeRepositoryTests: XCTestCase { func testFetchNoticeList() async throws { let date = Date() XCTAssertEqual(remoteNoticeDataSource.fetchNoticeListCallCount, 0) - let expected = [NoticeModel(id: 1, title: "title2", content: "contents", roles: .member, createdTime: date)] + let expected = [NoticeEntity(id: 1, title: "title2", content: "contents", roles: .member, createdTime: date)] remoteNoticeDataSource.fetchNoticeListReturn = expected let actual = try await sut.fetchNoticeList() @@ -29,4 +29,24 @@ final class NoticeRepositoryTests: XCTestCase { XCTAssertEqual(remoteNoticeDataSource.fetchNoticeListCallCount, 1) XCTAssertEqual(actual, expected) } + + func testFetchNotice() async throws { + let date = Date() + XCTAssertEqual(remoteNoticeDataSource.fetchNoticeCallCount, 0) + let expected = DetailNoticeEntity(id: 1, title: "title2", content: "contents", role: .member, images: [], createdDate: date) + remoteNoticeDataSource.fetchNoticeHandler = { _ in expected } + + let actual = try await sut.fetchNotice(id: 1) + + XCTAssertEqual(remoteNoticeDataSource.fetchNoticeCallCount, 1) + XCTAssertEqual(actual, expected) + } + + func testRemoveNotice() async throws { + XCTAssertEqual(remoteNoticeDataSource.removeNoticeCallCount, 0) + + try await sut.removeNotice(id: 1) + + XCTAssertEqual(remoteNoticeDataSource.removeNoticeCallCount, 1) + } } diff --git a/Projects/Domain/NoticeDomain/Tests/RemoveNoticeUseCaseTests.swift b/Projects/Domain/NoticeDomain/Tests/RemoveNoticeUseCaseTests.swift new file mode 100644 index 00000000..204f0170 --- /dev/null +++ b/Projects/Domain/NoticeDomain/Tests/RemoveNoticeUseCaseTests.swift @@ -0,0 +1,27 @@ +import NoticeDomainInterface +import XCTest +@testable import NoticeDomain +@testable import NoticeDomainTesting + +final class RemoveNoticeUseCaseTests: XCTestCase { + var noticeRepository: NoticeRepositorySpy! + var sut: RemoveNoticeUseCaseImpl! + + override func setUp() { + noticeRepository = .init() + sut = .init(noticeRepository: noticeRepository) + } + + override func tearDown() { + noticeRepository = nil + sut = nil + } + + func testCallAsFunction() async throws { + XCTAssertEqual(noticeRepository.removeNoticeCallCount, 0) + + try await sut(id: 1) + + XCTAssertEqual(noticeRepository.removeNoticeCallCount, 1) + } +} diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Assembly/DetailNoticeAssembly.swift b/Projects/Feature/DetailNoticeFeature/Sources/Assembly/DetailNoticeAssembly.swift index 2d455ec0..b61ec0cf 100644 --- a/Projects/Feature/DetailNoticeFeature/Sources/Assembly/DetailNoticeAssembly.swift +++ b/Projects/Feature/DetailNoticeFeature/Sources/Assembly/DetailNoticeAssembly.swift @@ -1,12 +1,14 @@ -import Swinject import NoticeDomainInterface +import Swinject +import UserDomainInterface public final class DetailNoticeAssembly: Assembly { public init() {} public func assemble(container: Container) { container.register(DetailNoticeFactory.self) { resolver in DetailNoticeFactoryImpl( - fetchNoticeUseCase: resolver.resolve(FetchNoticeUseCase.self)! + fetchNoticeUseCase: resolver.resolve(FetchNoticeUseCase.self)!, + loadCurrentUserRole: resolver.resolve(LoadCurrentUserRoleUseCase.self)! ) } } diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactoryImpl.swift b/Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactoryImpl.swift index 316f1f17..696379a7 100644 --- a/Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactoryImpl.swift +++ b/Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactoryImpl.swift @@ -1,18 +1,25 @@ import BaseFeature import NoticeDomainInterface import UIKit +import UserDomainInterface struct DetailNoticeFactoryImpl: DetailNoticeFactory { private let fetchNoticeUseCase: any FetchNoticeUseCase + private let loadCurrentUserRole: any LoadCurrentUserRoleUseCase - init(fetchNoticeUseCase: any FetchNoticeUseCase) { + init( + fetchNoticeUseCase: any FetchNoticeUseCase, + loadCurrentUserRole: any LoadCurrentUserRoleUseCase + ) { self.fetchNoticeUseCase = fetchNoticeUseCase + self.loadCurrentUserRole = loadCurrentUserRole } func makeViewController(noticeID: Int) -> any StoredViewControllable { let store = DetailNoticeStore( noticeID: noticeID, - fetchNoticeUseCase: fetchNoticeUseCase + fetchNoticeUseCase: fetchNoticeUseCase, + loadCurrentUserRole: loadCurrentUserRole ) return DetailNoticeViewController(store: store) } diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift index 03d98bd4..bc2d32e5 100644 --- a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift +++ b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift @@ -1,8 +1,10 @@ +import BaseDomainInterface import BaseFeature import Combine import Moordinator import NoticeDomainInterface import Store +import UserDomainInterface final class DetailNoticeStore: BaseStore { var route: PassthroughSubject = .init() @@ -11,33 +13,42 @@ final class DetailNoticeStore: BaseStore { var stateSubject: CurrentValueSubject private let noticeID: Int private let fetchNoticeUseCase: any FetchNoticeUseCase + private let loadCurrentUserRole: any LoadCurrentUserRoleUseCase init( noticeID: Int, - fetchNoticeUseCase: any FetchNoticeUseCase + fetchNoticeUseCase: any FetchNoticeUseCase, + loadCurrentUserRole: any LoadCurrentUserRoleUseCase ) { self.initialState = .init() self.stateSubject = .init(initialState) self.noticeID = noticeID self.fetchNoticeUseCase = fetchNoticeUseCase + self.loadCurrentUserRole = loadCurrentUserRole } struct State { var detailNotice: DetailNoticeModel? var isLoading = false + var currentUserRole = UserRoleType.member } enum Action { + case viewDidLoad case viewWillAppear } enum Mutation { case updateDetailNotice(DetailNoticeModel) case updateIsLoading(Bool) + case updateCurrentUserRole(UserRoleType) } } extension DetailNoticeStore { func mutate(state: State, action: Action) -> SideEffect { switch action { + case .viewDidLoad: + return self.viewDidLoad() + case .viewWillAppear: return self.viewWillAppear() } @@ -54,6 +65,9 @@ extension DetailNoticeStore { case let .updateIsLoading(isLoading): newState.isLoading = isLoading + + case let .updateCurrentUserRole(userRole): + newState.currentUserRole = userRole } return newState } @@ -61,6 +75,15 @@ extension DetailNoticeStore { // MARK: - Mutate private extension DetailNoticeStore { + func viewDidLoad() -> SideEffect { + return SideEffect + .just(try? loadCurrentUserRole()) + .replaceNil(with: .member) + .setFailureType(to: Never.self) + .map(Mutation.updateCurrentUserRole) + .eraseToSideEffect() + } + func viewWillAppear() -> SideEffect { let detailNoticeEffect = SideEffect .tryAsync { [noticeID, fetchNoticeUseCase] in diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift index 131bf700..94d78efa 100644 --- a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift +++ b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift @@ -14,10 +14,10 @@ final class DetailNoticeViewController: BaseStoredViewController { sharedState .map(\.currentUserRole) - .map { $0 == .member } .removeDuplicates() + .map { $0 == .member } .sink { [editButton, writeOrRemoveButton] isMember in editButton.isHidden = isMember writeOrRemoveButton.isHidden = isMember From c62ec3ef7e2170560511d26052eba745f11d3a2a Mon Sep 17 00:00:00 2001 From: baegteun Date: Sat, 29 Jul 2023 22:07:25 +0900 Subject: [PATCH 08/14] =?UTF-8?q?:sparkles:=20::=20[#122]=20DetailNoticeFe?= =?UTF-8?q?ature=20/=20=EA=B6=8C=ED=95=9C=EC=97=90=20=EB=94=B0=EB=9D=BC=20?= =?UTF-8?q?=EA=B3=B5=EC=A7=80=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=B6=80=EC=97=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Assembly/NoticeDomainAssembly.swift | 4 ++ .../BaseFeature/Sources/DotoriRoutePath.swift | 2 +- .../Demo/Sources/AppDelegate.swift | 7 ++- .../Assembly/DetailNoticeAssembly.swift | 3 +- .../Factory/DetailNoticeFactoryImpl.swift | 12 +++-- .../Sources/Scene/DetailNoticeStore.swift | 36 +++++++++++-- .../Scene/DetailNoticeViewController.swift | 7 ++- .../Sources/Moordinator/HomeMoordinator.swift | 32 +++++++----- .../Sources/Scene/Store/HomeStore.swift | 18 +++---- Projects/Feature/NoticeFeature/Project.swift | 1 + .../Sources/Assembly/NoticeAssembly.swift | 2 + .../Sources/Factory/NoticeFactoryImpl.swift | 5 ++ .../Moordinator/NoticeMoordinator.swift | 39 ++++++++++++++- .../Sources/Scene/NoticeViewController.swift | 1 - .../UIBarButtonItem+publisher.swift | 50 +++++++++++++++++++ .../Resources/en.lproj/Notice.strings | 3 ++ .../Resources/ko.lproj/Notice.strings | 3 ++ 17 files changed, 190 insertions(+), 35 deletions(-) create mode 100644 Projects/Shared/CombineUtility/Sources/UIBarButtonItem/UIBarButtonItem+publisher.swift diff --git a/Projects/Domain/NoticeDomain/Sources/Assembly/NoticeDomainAssembly.swift b/Projects/Domain/NoticeDomain/Sources/Assembly/NoticeDomainAssembly.swift index 8954fdc8..4ceffdaf 100644 --- a/Projects/Domain/NoticeDomain/Sources/Assembly/NoticeDomainAssembly.swift +++ b/Projects/Domain/NoticeDomain/Sources/Assembly/NoticeDomainAssembly.swift @@ -22,5 +22,9 @@ public final class NoticeDomainAssembly: Assembly { container.register(FetchNoticeUseCase.self) { resolver in FetchNoticeUseCaseImpl(noticeRepository: resolver.resolve(NoticeRepository.self)!) } + + container.register(RemoveNoticeUseCase.self) { resolver in + RemoveNoticeUseCaseImpl(noticeRepository: resolver.resolve(NoticeRepository.self)!) + } } } diff --git a/Projects/Feature/BaseFeature/Sources/DotoriRoutePath.swift b/Projects/Feature/BaseFeature/Sources/DotoriRoutePath.swift index b7be254e..c754fba9 100644 --- a/Projects/Feature/BaseFeature/Sources/DotoriRoutePath.swift +++ b/Projects/Feature/BaseFeature/Sources/DotoriRoutePath.swift @@ -11,7 +11,7 @@ public enum DotoriRoutePath: RoutePath { ) case confirmationDialog( title: String = "", - message: String = "", + description: String = "", confirmAction: () async -> Void ) case dismiss diff --git a/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift index b870a4f8..0b47c85b 100644 --- a/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift @@ -2,6 +2,7 @@ import Inject import UIKit @testable import DetailNoticeFeature @testable import NoticeDomainTesting +@testable import UserDomainTesting @main final class AppDelegate: UIResponder, UIApplicationDelegate { @@ -13,8 +14,12 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { ) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let fetchNoticeUseCase = FetchNoticeUseCaseSpy() + let loadCurrentUserRoleUseCase = LoadCurrentUserRoleUseCaseSpy() + loadCurrentUserRoleUseCase.loadCurrentUserRoleReturn = .developer let store = DetailNoticeStore( - fetchNoticeUseCase: fetchNoticeUseCase + noticeID: 1, + fetchNoticeUseCase: fetchNoticeUseCase, + loadCurrentUserRoleUseCase: loadCurrentUserRoleUseCase ) let viewController = Inject.ViewControllerHost( UINavigationController(rootViewController: DetailNoticeViewController(store: store)) diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Assembly/DetailNoticeAssembly.swift b/Projects/Feature/DetailNoticeFeature/Sources/Assembly/DetailNoticeAssembly.swift index b61ec0cf..083b66ee 100644 --- a/Projects/Feature/DetailNoticeFeature/Sources/Assembly/DetailNoticeAssembly.swift +++ b/Projects/Feature/DetailNoticeFeature/Sources/Assembly/DetailNoticeAssembly.swift @@ -8,7 +8,8 @@ public final class DetailNoticeAssembly: Assembly { container.register(DetailNoticeFactory.self) { resolver in DetailNoticeFactoryImpl( fetchNoticeUseCase: resolver.resolve(FetchNoticeUseCase.self)!, - loadCurrentUserRole: resolver.resolve(LoadCurrentUserRoleUseCase.self)! + removeNoticeUseCase: resolver.resolve(RemoveNoticeUseCase.self)!, + loadCurrentUserRoleUseCase: resolver.resolve(LoadCurrentUserRoleUseCase.self)! ) } } diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactoryImpl.swift b/Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactoryImpl.swift index 696379a7..8f872d30 100644 --- a/Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactoryImpl.swift +++ b/Projects/Feature/DetailNoticeFeature/Sources/Factory/DetailNoticeFactoryImpl.swift @@ -5,21 +5,25 @@ import UserDomainInterface struct DetailNoticeFactoryImpl: DetailNoticeFactory { private let fetchNoticeUseCase: any FetchNoticeUseCase - private let loadCurrentUserRole: any LoadCurrentUserRoleUseCase + private let removeNoticeUseCase: any RemoveNoticeUseCase + private let loadCurrentUserRoleUseCase: any LoadCurrentUserRoleUseCase init( fetchNoticeUseCase: any FetchNoticeUseCase, - loadCurrentUserRole: any LoadCurrentUserRoleUseCase + removeNoticeUseCase: any RemoveNoticeUseCase, + loadCurrentUserRoleUseCase: any LoadCurrentUserRoleUseCase ) { self.fetchNoticeUseCase = fetchNoticeUseCase - self.loadCurrentUserRole = loadCurrentUserRole + self.removeNoticeUseCase = removeNoticeUseCase + self.loadCurrentUserRoleUseCase = loadCurrentUserRoleUseCase } func makeViewController(noticeID: Int) -> any StoredViewControllable { let store = DetailNoticeStore( noticeID: noticeID, fetchNoticeUseCase: fetchNoticeUseCase, - loadCurrentUserRole: loadCurrentUserRole + removeNoticeUseCase: removeNoticeUseCase, + loadCurrentUserRoleUseCase: loadCurrentUserRoleUseCase ) return DetailNoticeViewController(store: store) } diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift index bc2d32e5..9ea3aae4 100644 --- a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift +++ b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeStore.swift @@ -1,9 +1,12 @@ import BaseDomainInterface import BaseFeature import Combine +import DesignSystem +import Localization import Moordinator import NoticeDomainInterface import Store +import UIKit import UserDomainInterface final class DetailNoticeStore: BaseStore { @@ -13,18 +16,21 @@ final class DetailNoticeStore: BaseStore { var stateSubject: CurrentValueSubject private let noticeID: Int private let fetchNoticeUseCase: any FetchNoticeUseCase - private let loadCurrentUserRole: any LoadCurrentUserRoleUseCase + private let removeNoticeUseCase: any RemoveNoticeUseCase + private let loadCurrentUserRoleUseCase: any LoadCurrentUserRoleUseCase init( noticeID: Int, fetchNoticeUseCase: any FetchNoticeUseCase, - loadCurrentUserRole: any LoadCurrentUserRoleUseCase + removeNoticeUseCase: any RemoveNoticeUseCase, + loadCurrentUserRoleUseCase: any LoadCurrentUserRoleUseCase ) { self.initialState = .init() self.stateSubject = .init(initialState) self.noticeID = noticeID self.fetchNoticeUseCase = fetchNoticeUseCase - self.loadCurrentUserRole = loadCurrentUserRole + self.removeNoticeUseCase = removeNoticeUseCase + self.loadCurrentUserRoleUseCase = loadCurrentUserRoleUseCase } struct State { @@ -35,6 +41,7 @@ final class DetailNoticeStore: BaseStore { enum Action { case viewDidLoad case viewWillAppear + case removeBarButtonDidTap } enum Mutation { case updateDetailNotice(DetailNoticeModel) @@ -51,6 +58,9 @@ extension DetailNoticeStore { case .viewWillAppear: return self.viewWillAppear() + + case .removeBarButtonDidTap: + return self.removeBarButtonDidTap() } return .none } @@ -77,7 +87,7 @@ extension DetailNoticeStore { private extension DetailNoticeStore { func viewDidLoad() -> SideEffect { return SideEffect - .just(try? loadCurrentUserRole()) + .just(try? loadCurrentUserRoleUseCase()) .replaceNil(with: .member) .setFailureType(to: Never.self) .map(Mutation.updateCurrentUserRole) @@ -93,6 +103,24 @@ private extension DetailNoticeStore { .map(Mutation.updateDetailNotice) return self.makeLoadingSideEffect(detailNoticeEffect) } + + func removeBarButtonDidTap() -> SideEffect { + let confirmRoutePath = DotoriRoutePath.confirmationDialog( + title: L10n.Notice.removeNoticeDialogTitle, + description: L10n.Notice.removeNoticeDialogDescription + ) { [removeNoticeUseCase, noticeID, route] in + do { + try await removeNoticeUseCase(id: noticeID) + route.send(DotoriRoutePath.dismiss) + route.send(DotoriRoutePath.pop) + await DotoriToast.makeToast(text: L10n.Notice.completeToRemoveNotice, style: .success) + } catch { + await DotoriToast.makeToast(text: error.localizedDescription, style: .error) + } + } + route.send(confirmRoutePath) + return .none + } } // MARK: - Reusable diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift index 94d78efa..effcc02b 100644 --- a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift +++ b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift @@ -42,7 +42,7 @@ final class DetailNoticeViewController: BaseStoredViewController Void + ) -> MoordinatorContributors { + let viewController = confirmationDialogFactory.makeViewController( + title: title, + description: description, + confirmAction: confirmAction + ) + self.rootVC.modalPresent(viewController) + return .one( + .contribute( + withNextPresentable: viewController, + withNextRouter: viewController.store + ) + ) + } + func presentToAlert( title: String?, message: String?, diff --git a/Projects/Feature/HomeFeature/Sources/Scene/Store/HomeStore.swift b/Projects/Feature/HomeFeature/Sources/Scene/Store/HomeStore.swift index 9f460c13..08f47dda 100644 --- a/Projects/Feature/HomeFeature/Sources/Scene/Store/HomeStore.swift +++ b/Projects/Feature/HomeFeature/Sources/Scene/Store/HomeStore.swift @@ -163,7 +163,7 @@ private extension HomeStore { .init(title: L10n.Home.logoutButtonTitle, style: .default) { [route, logoutUseCase] _ in let confirmationDialogRoutePath = DotoriRoutePath.confirmationDialog( title: L10n.Home.logoutTitle, - message: L10n.Home.reallyLogoutTitle + description: L10n.Home.reallyLogoutTitle ) { logoutUseCase() route.send(DotoriRoutePath.signin) @@ -177,15 +177,15 @@ private extension HomeStore { } func applySelfStudyButtonDidTap() { -// guard currentState.currentUserRole == .member else { -// return -// } + guard currentState.currentUserRole == .member else { + return + } #warning("자습 인원 수정 로직 추가") Task.catching { if self.currentState.selfStudyStatus == .applied { let confirmRoutePath = DotoriRoutePath.confirmationDialog( title: "자습 신청 취소", - message: "정말 자습 신청을 취소하시겠습니까?" + description: "정말 자습 신청을 취소하시겠습니까?" ) { [cancelSelfStudyUseCase = self.cancelSelfStudyUseCase] in try? await cancelSelfStudyUseCase() await DotoriToast.makeToast(text: "자습을 취소하였습니다.", style: .success) @@ -202,15 +202,15 @@ private extension HomeStore { } func applyMassageButtonDidTap() { -// guard currentState.currentUserRole == .member else { -// return -// } + guard currentState.currentUserRole == .member else { + return + } #warning("안마 인원 수정 로직 추가") Task.catching { if self.currentState.massageStatus == .applied { let confirmRoutePath = DotoriRoutePath.confirmationDialog( title: "안마의자 신청 취소", - message: "정말 안마의자 신청을 취소하시겠습니까?" + description: "정말 안마의자 신청을 취소하시겠습니까?" ) { [cancelMassageUseCase = self.cancelMassageUseCase] in try? await cancelMassageUseCase() await DotoriToast.makeToast(text: "안마의자를 취소하였습니다.", style: .success) diff --git a/Projects/Feature/NoticeFeature/Project.swift b/Projects/Feature/NoticeFeature/Project.swift index 0adc7d8d..c9f0ba24 100644 --- a/Projects/Feature/NoticeFeature/Project.swift +++ b/Projects/Feature/NoticeFeature/Project.swift @@ -8,6 +8,7 @@ let project = Project.module( .implements(module: .feature(.NoticeFeature), dependencies: [ .feature(target: .BaseFeature), .feature(target: .DetailNoticeFeature), + .feature(target: .ConfirmationDialogFeature), .domain(target: .NoticeDomain, type: .interface), .domain(target: .UserDomain, type: .interface) ]), diff --git a/Projects/Feature/NoticeFeature/Sources/Assembly/NoticeAssembly.swift b/Projects/Feature/NoticeFeature/Sources/Assembly/NoticeAssembly.swift index 2a2ad289..cba6af81 100644 --- a/Projects/Feature/NoticeFeature/Sources/Assembly/NoticeAssembly.swift +++ b/Projects/Feature/NoticeFeature/Sources/Assembly/NoticeAssembly.swift @@ -1,3 +1,4 @@ +import ConfirmationDialogFeature import DetailNoticeFeature import NoticeDomainInterface import Swinject @@ -10,6 +11,7 @@ public final class NoticeAssembly: Assembly { NoticeFactoryImpl( fetchNoticeListUseCase: resolver.resolve(FetchNoticeListUseCase.self)!, loadCurrentUserRoleUseCase: resolver.resolve(LoadCurrentUserRoleUseCase.self)!, + confirmationDialogFactory: resolver.resolve(ConfirmationDialogFactory.self)!, detailNoticeFactory: resolver.resolve(DetailNoticeFactory.self)! ) } diff --git a/Projects/Feature/NoticeFeature/Sources/Factory/NoticeFactoryImpl.swift b/Projects/Feature/NoticeFeature/Sources/Factory/NoticeFactoryImpl.swift index c47f6371..0dfd7e41 100644 --- a/Projects/Feature/NoticeFeature/Sources/Factory/NoticeFactoryImpl.swift +++ b/Projects/Feature/NoticeFeature/Sources/Factory/NoticeFactoryImpl.swift @@ -1,3 +1,4 @@ +import ConfirmationDialogFeature import DetailNoticeFeature import Moordinator import NoticeDomainInterface @@ -6,15 +7,18 @@ import UserDomainInterface struct NoticeFactoryImpl: NoticeFactory { private let fetchNoticeListUseCase: any FetchNoticeListUseCase private let loadCurrentUserRoleUseCase: any LoadCurrentUserRoleUseCase + private let confirmationDialogFactory: any ConfirmationDialogFactory private let detailNoticeFactory: any DetailNoticeFactory init( fetchNoticeListUseCase: any FetchNoticeListUseCase, loadCurrentUserRoleUseCase: any LoadCurrentUserRoleUseCase, + confirmationDialogFactory: any ConfirmationDialogFactory, detailNoticeFactory: any DetailNoticeFactory ) { self.fetchNoticeListUseCase = fetchNoticeListUseCase self.loadCurrentUserRoleUseCase = loadCurrentUserRoleUseCase + self.confirmationDialogFactory = confirmationDialogFactory self.detailNoticeFactory = detailNoticeFactory } @@ -26,6 +30,7 @@ struct NoticeFactoryImpl: NoticeFactory { let noticeViewController = NoticeViewController(store: noticeStore) return NoticeMoordinator( noticeViewController: noticeViewController, + confirmationDialogFactory: confirmationDialogFactory, detailNoticeFactory: detailNoticeFactory ) } diff --git a/Projects/Feature/NoticeFeature/Sources/Moordinator/NoticeMoordinator.swift b/Projects/Feature/NoticeFeature/Sources/Moordinator/NoticeMoordinator.swift index 930c2b80..d38e52c5 100644 --- a/Projects/Feature/NoticeFeature/Sources/Moordinator/NoticeMoordinator.swift +++ b/Projects/Feature/NoticeFeature/Sources/Moordinator/NoticeMoordinator.swift @@ -1,4 +1,5 @@ import BaseFeature +import ConfirmationDialogFeature import DWebKit import DetailNoticeFeature import Moordinator @@ -7,6 +8,7 @@ import UIKit final class NoticeMoordinator: Moordinator { private let rootVC = UINavigationController() private let noticeViewController: any StoredViewControllable + private let confirmationDialogFactory: any ConfirmationDialogFactory private let detailNoticeFactory: any DetailNoticeFactory var root: Presentable { rootVC @@ -14,9 +16,11 @@ final class NoticeMoordinator: Moordinator { init( noticeViewController: any StoredViewControllable, + confirmationDialogFactory: any ConfirmationDialogFactory, detailNoticeFactory: any DetailNoticeFactory ) { self.noticeViewController = noticeViewController + self.confirmationDialogFactory = confirmationDialogFactory self.detailNoticeFactory = detailNoticeFactory } @@ -29,6 +33,15 @@ final class NoticeMoordinator: Moordinator { case let .detailNotice(noticeID): return navigateToDetailNotice(noticeID: noticeID) + case let .confirmationDialog(title, description, confirmAction): + return presentToConfirmationDialog(title: title, description: description, confirmAction: confirmAction) + + case .dismiss: + self.rootVC.presentedViewController?.dismiss(animated: true) + + case .pop: + self.rootVC.popViewController(animated: true) + default: return .none } @@ -50,6 +63,30 @@ private extension NoticeMoordinator { func navigateToDetailNotice(noticeID: Int) -> MoordinatorContributors { let viewController = detailNoticeFactory.makeViewController(noticeID: noticeID) self.rootVC.pushViewController(viewController, animated: true) - return .none + return .one( + .contribute( + withNextPresentable: viewController, + withNextRouter: viewController.store + ) + ) + } + + func presentToConfirmationDialog( + title: String, + description: String, + confirmAction: @escaping () async -> Void + ) -> MoordinatorContributors { + let viewController = confirmationDialogFactory.makeViewController( + title: title, + description: description, + confirmAction: confirmAction + ) + self.rootVC.modalPresent(viewController) + return .one( + .contribute( + withNextPresentable: viewController, + withNextRouter: viewController.store + ) + ) } } diff --git a/Projects/Feature/NoticeFeature/Sources/Scene/NoticeViewController.swift b/Projects/Feature/NoticeFeature/Sources/Scene/NoticeViewController.swift index 29e83d93..5b11cdf5 100644 --- a/Projects/Feature/NoticeFeature/Sources/Scene/NoticeViewController.swift +++ b/Projects/Feature/NoticeFeature/Sources/Scene/NoticeViewController.swift @@ -32,7 +32,6 @@ final class NoticeViewController: BaseStoredViewController { .set(\.backgroundColor, .clear) .set(\.separatorStyle, .none) .set(\.sectionHeaderTopPadding, 0) - .set(\.sectionHeaderHeight, 0) .set(\.allowsMultipleSelection, false) .then { $0.register(cellType: NoticeCell.self) diff --git a/Projects/Shared/CombineUtility/Sources/UIBarButtonItem/UIBarButtonItem+publisher.swift b/Projects/Shared/CombineUtility/Sources/UIBarButtonItem/UIBarButtonItem+publisher.swift new file mode 100644 index 00000000..dd7b7ace --- /dev/null +++ b/Projects/Shared/CombineUtility/Sources/UIBarButtonItem/UIBarButtonItem+publisher.swift @@ -0,0 +1,50 @@ +import Combine +import UIKit + +public extension UIBarButtonItem { + final class Subscription: Combine.Subscription where SubscriberType.Input == Input { + private var subscriber: SubscriberType? + private let input: Input + + public init(subscriber: SubscriberType, input: Input) { + self.subscriber = subscriber + self.input = input + input.target = self + input.action = #selector(eventHandler) + } + + public func request(_ demand: Subscribers.Demand) {} + + public func cancel() { + subscriber = nil + } + + @objc private func eventHandler() { + _ = subscriber?.receive(input) + } + } + + struct Publisher: Combine.Publisher { + public typealias Output = Output + public typealias Failure = Never + + let output: Output + + public init(output: Output) { + self.output = output + } + + public func receive(subscriber: S) where S: Subscriber, Self.Failure == S.Failure, Self.Output == S.Input { + let subscription = Subscription(subscriber: subscriber, input: output) + subscriber.receive(subscription: subscription) + } + } +} + +public extension UIBarButtonItem { + var tapPublisher: AnyPublisher { + Publisher(output: self) + .map { _ in } + .eraseToAnyPublisher() + } +} diff --git a/Projects/UserInterface/Localization/Resources/en.lproj/Notice.strings b/Projects/UserInterface/Localization/Resources/en.lproj/Notice.strings index 6886ff89..2a4ac4bd 100644 --- a/Projects/UserInterface/Localization/Resources/en.lproj/Notice.strings +++ b/Projects/UserInterface/Localization/Resources/en.lproj/Notice.strings @@ -16,3 +16,6 @@ "write_button_title" = "+ Write"; "remove_button_title" = "Remove"; "detail_notice_date_format" = "MMM dd, yyyy"; +"remove_notice_dialog_title" = "Remove the notice"; +"remove_notice_dialog_description" = "Are you sure you want to delete the notice?"; +"complete_to_remove_notice" = "Complete to remove the notice"; diff --git a/Projects/UserInterface/Localization/Resources/ko.lproj/Notice.strings b/Projects/UserInterface/Localization/Resources/ko.lproj/Notice.strings index ff587ece..0e57cd35 100644 --- a/Projects/UserInterface/Localization/Resources/ko.lproj/Notice.strings +++ b/Projects/UserInterface/Localization/Resources/ko.lproj/Notice.strings @@ -16,3 +16,6 @@ "write_button_title" = "+ 작성"; "remove_button_title" = "삭제"; "detail_notice_date_format" = "yyyy년 MM월 dd일"; +"remove_notice_dialog_title" = "공지사항 삭제"; +"remove_notice_dialog_description" = "정말로 공지사항을 삭제하시겠습니까?"; +"complete_to_remove_notice" = "공지사항 삭제 완료"; From efba3ba9b9abff8df32fc65d6f83562bedba3ed8 Mon Sep 17 00:00:00 2001 From: baegteun Date: Sat, 29 Jul 2023 22:19:30 +0900 Subject: [PATCH 09/14] =?UTF-8?q?:sparkles:=20::=20[#122]=20DetailNoticeFe?= =?UTF-8?q?ature=20/=20=EC=83=81=EC=84=B8=20=EA=B3=B5=EC=A7=80=20=EC=A0=9C?= =?UTF-8?q?=EB=AA=A9,=20=EC=88=98=EC=A0=95=EC=9D=BC=20=EB=A7=90=EA=B3=A0?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=EC=9D=BC=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Scene/DetailNoticeViewController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift index effcc02b..c0f147e1 100644 --- a/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift +++ b/Projects/Feature/DetailNoticeFeature/Sources/Scene/DetailNoticeViewController.swift @@ -26,6 +26,7 @@ final class DetailNoticeViewController: BaseStoredViewController Date: Sat, 29 Jul 2023 22:31:08 +0900 Subject: [PATCH 10/14] =?UTF-8?q?:white=5Fcheck=5Fmark:=20::=20[#122]=20De?= =?UTF-8?q?tailNoticeFeature=20/=20=EA=B3=B5=EC=A7=80=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=20=EB=B0=8F=20=ED=95=B4=EC=A0=9C=20=EC=95=A1=EC=85=98=20Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tests/NoticeFeatureTest.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Projects/Feature/NoticeFeature/Tests/NoticeFeatureTest.swift b/Projects/Feature/NoticeFeature/Tests/NoticeFeatureTest.swift index cc8ec71d..d8d2e16c 100644 --- a/Projects/Feature/NoticeFeature/Tests/NoticeFeatureTest.swift +++ b/Projects/Feature/NoticeFeature/Tests/NoticeFeatureTest.swift @@ -98,7 +98,7 @@ final class NoticeFeatureTests: XCTestCase { XCTAssertEqual(sut.currentState.isEditingMode, true) } - func test_RouteNoticeDetail_When_NoticeDidTap_And_IsEditingFalse() { + func test_RouteNoticeDetail_When_NoticeDidSelect_And_IsEditingFalse() { let expectedNoticeID = 1 XCTAssertEqual(sut.currentState.isEditingMode, false) let expectation = XCTestExpectation(description: "route expectation") @@ -110,12 +110,12 @@ final class NoticeFeatureTests: XCTestCase { } .store(in: &subscription) - sut.send(.noticeDidTap(expectedNoticeID)) + sut.send(.noticeDidSelect(expectedNoticeID)) wait(for: [expectation], timeout: 1.0) guard let latestRoutePath = latestRoutePath?.asDotori, - case let .noticeDetail(noticeID) = latestRoutePath, + case let .detailNotice(noticeID) = latestRoutePath, noticeID == expectedNoticeID else { XCTFail("latestRoutePath is not DotoriRoutePath.noticeDetail") @@ -123,25 +123,25 @@ final class NoticeFeatureTests: XCTestCase { } } - func test_InsertSelectedNotice_When_NoticeDidTap_And_IsEditingTrue() { + func test_InsertSelectedNotice_When_NoticeDidSelectAndDeselect_And_IsEditingTrue() { sut.send(.editButtonDidTap) XCTAssertEqual(sut.currentState.isEditingMode, true) let noticeIDOne = 1 - sut.send(.noticeDidTap(noticeIDOne)) + sut.send(.noticeDidSelect(noticeIDOne)) XCTAssertEqual([noticeIDOne], sut.currentState.selectedNotice) let noticeIDTwo = 2 - sut.send(.noticeDidTap(noticeIDTwo)) + sut.send(.noticeDidSelect(noticeIDTwo)) XCTAssertEqual([noticeIDOne, noticeIDTwo], sut.currentState.selectedNotice) - sut.send(.noticeDidTap(noticeIDOne)) - XCTAssertEqual([noticeIDTwo], sut.currentState.selectedNotice) - - sut.send(.noticeDidTap(noticeIDOne)) + sut.send(.noticeDidSelect(noticeIDOne)) XCTAssertEqual([noticeIDOne, noticeIDTwo], sut.currentState.selectedNotice) + sut.send(.noticeDidDeselect(noticeIDOne)) + XCTAssertEqual([noticeIDTwo], sut.currentState.selectedNotice) + sut.send(.editButtonDidTap) XCTAssertEqual([], sut.currentState.selectedNotice) } From 3c10e5212d4b2b195cb61248214d7ff6bc7dc046 Mon Sep 17 00:00:00 2001 From: baegteun Date: Sat, 29 Jul 2023 22:37:49 +0900 Subject: [PATCH 11/14] :white_check_mark: :: [#122] DetailNoticeFeature / removeNoticeUseCase DI --- .../DetailNoticeFeature/Tests/DetailNoticeFeatureTest.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Projects/Feature/DetailNoticeFeature/Tests/DetailNoticeFeatureTest.swift b/Projects/Feature/DetailNoticeFeature/Tests/DetailNoticeFeatureTest.swift index 68e71494..e0224ce4 100644 --- a/Projects/Feature/DetailNoticeFeature/Tests/DetailNoticeFeatureTest.swift +++ b/Projects/Feature/DetailNoticeFeature/Tests/DetailNoticeFeatureTest.swift @@ -5,21 +5,25 @@ import XCTest final class DetailNoticeFeatureTests: XCTestCase { var fetchNoticeUseCase: FetchNoticeUseCaseSpy! + var removeNoticeUseCase: RemoveNoticeUseCaseSpy! var loadCurrentUserRoleUseCase: LoadCurrentUserRoleUseCaseSpy! var sut: DetailNoticeStore! override func setUp() { fetchNoticeUseCase = .init() + removeNoticeUseCase = .init() loadCurrentUserRoleUseCase = .init() sut = .init( noticeID: 1, fetchNoticeUseCase: fetchNoticeUseCase, - loadCurrentUserRole: loadCurrentUserRoleUseCase + removeNoticeUseCase: removeNoticeUseCase, + loadCurrentUserRoleUseCase: loadCurrentUserRoleUseCase ) } override func tearDown() { fetchNoticeUseCase = nil + removeNoticeUseCase = nil loadCurrentUserRoleUseCase = nil sut = nil } From 9fb2c8fab501a9876f05e00bc9300e3ccc5044a1 Mon Sep 17 00:00:00 2001 From: baegteun Date: Sat, 29 Jul 2023 22:43:26 +0900 Subject: [PATCH 12/14] =?UTF-8?q?:sparkles:=20::=20[#122]=20MusicFeature?= =?UTF-8?q?=20/=20=EB=82=A0=EC=A7=9C=20=EC=84=A0=ED=83=9D=20Bar=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=A0=9C=EC=99=B8=EC=8B=9C=EC=BC=9C?= =?UTF-8?q?=EB=86=93=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MusicFeature/Sources/Scene/MusicViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Projects/Feature/MusicFeature/Sources/Scene/MusicViewController.swift b/Projects/Feature/MusicFeature/Sources/Scene/MusicViewController.swift index ba5baa94..57f9cd4c 100644 --- a/Projects/Feature/MusicFeature/Sources/Scene/MusicViewController.swift +++ b/Projects/Feature/MusicFeature/Sources/Scene/MusicViewController.swift @@ -66,7 +66,8 @@ final class MusicViewController: BaseStoredViewController { override func configureNavigation() { self.navigationItem.setLeftBarButton(musicNavigationBarLabel, animated: true) - self.navigationItem.setRightBarButtonItems([newMusicButton, calendarBarButton], animated: true) + self.navigationItem.setRightBarButtonItems([newMusicButton], animated: true) + #warning("날짜 선택 구현") } override func bindAction() { From bad5f598905553700959564af8c312e7ca3eb311 Mon Sep 17 00:00:00 2001 From: baegteun Date: Sat, 29 Jul 2023 22:44:02 +0900 Subject: [PATCH 13/14] =?UTF-8?q?:pencil2:=20::=20[#122]=20DetailNoticeFea?= =?UTF-8?q?ture=20/=20DemoApp=EC=97=90=EB=8F=84=20=EC=9E=8A=EC=96=B4?= =?UTF-8?q?=EB=A8=B9=EC=9D=80=20DI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift index 0b47c85b..5dd91f23 100644 --- a/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Feature/DetailNoticeFeature/Demo/Sources/AppDelegate.swift @@ -14,11 +14,13 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { ) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let fetchNoticeUseCase = FetchNoticeUseCaseSpy() + let removeNoticeUseCase = RemoveNoticeUseCaseSpy() let loadCurrentUserRoleUseCase = LoadCurrentUserRoleUseCaseSpy() loadCurrentUserRoleUseCase.loadCurrentUserRoleReturn = .developer let store = DetailNoticeStore( noticeID: 1, fetchNoticeUseCase: fetchNoticeUseCase, + removeNoticeUseCase: removeNoticeUseCase, loadCurrentUserRoleUseCase: loadCurrentUserRoleUseCase ) let viewController = Inject.ViewControllerHost( From cf1b396b06684e41bd2b260d04cca67224778ed9 Mon Sep 17 00:00:00 2001 From: baegteun Date: Sun, 30 Jul 2023 13:02:47 +0900 Subject: [PATCH 14/14] =?UTF-8?q?:pencil2:=20::=20[#122]=20MusicFeature=20?= =?UTF-8?q?/=20=EB=B3=80=EC=88=98=EB=AA=85=20=EB=B0=94=EA=BE=BC=EA=B1=B0?= =?UTF-8?q?=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MusicFeature/Sources/Scene/MusicViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Feature/MusicFeature/Sources/Scene/MusicViewController.swift b/Projects/Feature/MusicFeature/Sources/Scene/MusicViewController.swift index 83dbc25c..270b6728 100644 --- a/Projects/Feature/MusicFeature/Sources/Scene/MusicViewController.swift +++ b/Projects/Feature/MusicFeature/Sources/Scene/MusicViewController.swift @@ -66,7 +66,7 @@ final class MusicViewController: BaseStoredViewController { override func configureNavigation() { self.navigationItem.setLeftBarButton(musicNavigationBarLabel, animated: true) - self.navigationItem.setRightBarButtonItems([newMusicButton], animated: true) + self.navigationItem.setRightBarButtonItems([proposeMusicButton], animated: true) #warning("날짜 선택 구현") }