diff --git a/Makefile b/Makefile index a6639be4..9fa7ab0c 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,14 @@ TUIST = tuist SWIFTLINT = swiftlint +FASTLANE = fastlane all: lint generate generate: + $(TUIST) install TUIST_ROOT_DIR=${PWD} $(TUIST) generate $(SWIFTLINT) autocorrect --fix + $(FASTLANE) match lint: $(SWIFTLINT) diff --git a/Projects/Data/ImageDataModule/ImageData/Resources/Empty.swift b/Projects/Data/ImageDataModule/ImageData/Resources/Empty.swift new file mode 100644 index 00000000..8e26c8b9 --- /dev/null +++ b/Projects/Data/ImageDataModule/ImageData/Resources/Empty.swift @@ -0,0 +1 @@ +// This file is used to generate the TuistBundle file. diff --git a/Projects/Data/ImageDataModule/ImageData/Sources/Error/ImageError.swift b/Projects/Data/ImageDataModule/ImageData/Sources/Error/ImageError.swift new file mode 100644 index 00000000..7dd1f315 --- /dev/null +++ b/Projects/Data/ImageDataModule/ImageData/Sources/Error/ImageError.swift @@ -0,0 +1,17 @@ +// +// ImageError.swift +// ImageData +// +// Created by DOYEON LEE on 8/25/24. +// + +import Foundation + +/// An error type that announcement related errors. +public enum ImageError: LocalizedError { + /// The response from the server is not match client scheme. + case invalidResponse + + /// Wraps another error that caused this error. + case underlying(_ error: Error) +} diff --git a/Projects/Data/ImageDataModule/ImageData/Sources/Repository/ImageRepository.swift b/Projects/Data/ImageDataModule/ImageData/Sources/Repository/ImageRepository.swift new file mode 100644 index 00000000..b79e9c31 --- /dev/null +++ b/Projects/Data/ImageDataModule/ImageData/Sources/Repository/ImageRepository.swift @@ -0,0 +1,72 @@ +// +// ImageRepository.swift +// ImageData +// +// Created by DOYEON LEE on 8/25/24. +// + +import OpenapiGenerated +import ImageDataInterface +import CommonData + +import OpenAPIURLSession +import RxSwift + +public struct ImageRepository: ImageRepositoryInterface { + private let client: APIProtocol + + public init() { + self.client = Client( + serverURL: UrlConfig.baseUrl.url, + configuration: .init(dateTranscoder: .custom), + transport: URLSessionTransport(), + middlewares: [AuthenticationMiddleware()] + ) + } + + func getImageUploadPath( + _ request: GetImageUploadPathRequest + ) -> Observable { + return Observable.create { observer in + Task { + do { + let response = try await client.getContentImage( + .init(query: request) + ) + + let data = try response.ok.body.json + + observer.onNext(data) + observer.onCompleted() + } catch { + observer.onError(ImageError.underlying(error)) + } + } + + return Disposables.create() + } + } + + func notifyImageUploadComplete( + _ request: NotifyImageUploadCompleteRequest + ) -> Observable { + return Observable.create { observer in + Task { + do { + let response = try await client.notifyContentImageSaveSuccess( + body: request + ) + + let data = try response.ok + + observer.onNext(()) + observer.onCompleted() + } catch { + observer.onError(ImageError.underlying(error)) + } + } + + return Disposables.create() + } + } +} diff --git a/Projects/Data/ImageDataModule/Project.swift b/Projects/Data/ImageDataModule/Project.swift new file mode 100644 index 00000000..9955caf0 --- /dev/null +++ b/Projects/Data/ImageDataModule/Project.swift @@ -0,0 +1,9 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.makeDataModule( + .image, + dependencies: [ + .dataInterface(.image) + ] +) diff --git a/Projects/DataInterface/ImageDataInterfaceModule/ImageDataInterface/Resources/Empty.swift b/Projects/DataInterface/ImageDataInterfaceModule/ImageDataInterface/Resources/Empty.swift new file mode 100644 index 00000000..8e26c8b9 --- /dev/null +++ b/Projects/DataInterface/ImageDataInterfaceModule/ImageDataInterface/Resources/Empty.swift @@ -0,0 +1 @@ +// This file is used to generate the TuistBundle file. diff --git a/Projects/DataInterface/ImageDataInterfaceModule/ImageDataInterface/Sources/ImageRepositoryInterface.swift b/Projects/DataInterface/ImageDataInterfaceModule/ImageDataInterface/Sources/ImageRepositoryInterface.swift new file mode 100644 index 00000000..f886a415 --- /dev/null +++ b/Projects/DataInterface/ImageDataInterfaceModule/ImageDataInterface/Sources/ImageRepositoryInterface.swift @@ -0,0 +1,18 @@ +// +// ImageRepositoryInterface.swift +// ImageDataInterface +// +// Created by DOYEON LEE on 8/25/24. +// + +import RxSwift + +public protocol ImageRepositoryInterface { + func getImageUploadPath( + _ request: GetImageUploadPathRequest + ) -> Observable + + func notifyImageUploadComplete( + _ request: NotifyImageUploadCompleteRequest + ) -> Observable +} diff --git a/Projects/DataInterface/ImageDataInterfaceModule/ImageDataInterface/Sources/Request&Response/GetImageUploadPathRequest&Response.swift b/Projects/DataInterface/ImageDataInterfaceModule/ImageDataInterface/Sources/Request&Response/GetImageUploadPathRequest&Response.swift new file mode 100644 index 00000000..b1cf07c1 --- /dev/null +++ b/Projects/DataInterface/ImageDataInterfaceModule/ImageDataInterface/Sources/Request&Response/GetImageUploadPathRequest&Response.swift @@ -0,0 +1,14 @@ +// +// GetImageUploadPathRequest&Response.swift +// ImageDataInterface +// +// Created by DOYEON LEE on 8/25/24. +// + +import OpenapiGenerated + +// MARK: Request +public typealias GetImageUploadPathRequest = Operations.getContentImage.Input.Query + +// MARK: Response +public typealias GetImageUploadPathResponse = Components.Schemas.ContentImagePresignedUrlVO diff --git a/Projects/DataInterface/ImageDataInterfaceModule/ImageDataInterface/Sources/Request&Response/NotifyImageUploadCompleteRequest&Response.swift b/Projects/DataInterface/ImageDataInterfaceModule/ImageDataInterface/Sources/Request&Response/NotifyImageUploadCompleteRequest&Response.swift new file mode 100644 index 00000000..5e8681f3 --- /dev/null +++ b/Projects/DataInterface/ImageDataInterfaceModule/ImageDataInterface/Sources/Request&Response/NotifyImageUploadCompleteRequest&Response.swift @@ -0,0 +1,14 @@ +// +// NotifyImageUploadCompleteRequest&Response.swift +// ImageDataInterface +// +// Created by DOYEON LEE on 8/25/24. +// + +import OpenapiGenerated + +// MARK: Request +public typealias NotifyImageUploadCompleteRequest = Operations.notifyContentImageSaveSuccess.Input.Body + +// MARK: Response +public typealias NotifyImageUploadCompleteResponse = Void diff --git a/Projects/DataInterface/ImageDataInterfaceModule/Project.swift b/Projects/DataInterface/ImageDataInterfaceModule/Project.swift new file mode 100644 index 00000000..9e8c3fa2 --- /dev/null +++ b/Projects/DataInterface/ImageDataInterfaceModule/Project.swift @@ -0,0 +1,4 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.makeDataInterfaceModule(.image) diff --git a/Projects/Domain/AnnouncementDomainModule/AnnouncementUsecase/Sources/Public/CreateAnnouncementUsecase.swift b/Projects/Domain/AnnouncementDomainModule/AnnouncementUsecase/Sources/Public/CreateAnnouncementUsecase.swift index 47289246..e1be29ce 100644 --- a/Projects/Domain/AnnouncementDomainModule/AnnouncementUsecase/Sources/Public/CreateAnnouncementUsecase.swift +++ b/Projects/Domain/AnnouncementDomainModule/AnnouncementUsecase/Sources/Public/CreateAnnouncementUsecase.swift @@ -42,13 +42,46 @@ public struct CreateAnnouncementUsecase { // MARK: Execute method public func execute(_ input: Input) -> Observable { + guard let member = memberUserDefaultsManager.get() else { + fatalError("해당 유즈케이스는 로그인 상태에서 실행되어야합니다.") + } + let newAnnouncement = input.newAnnouncement + + let autoGeneratedEndAt = generateAutoEndDate() - // endAt이 nil이면 한 달 뒤의 날짜로 설정 - let autoGeneratedEndAt = Calendar.current - .date(byAdding: .month, value: 1, to: Date())! + let (noticeBefore, noticeDate) = parseNotificationDates( + from: newAnnouncement.notifications + ) + + return announcementRepository + .createAnnouncement( + .init( + organizationId: newAnnouncement.organizationId, + memberId: member.id, + title: newAnnouncement.title, + content: newAnnouncement.body, + tasks: (newAnnouncement.todos ?? []) + .map { .init(content: $0) }, + endAt: newAnnouncement.endAt ?? autoGeneratedEndAt, + noticeBefore: noticeBefore, + noticeDate: noticeDate + ) + ) + .map { result in + Output(announcementId: Int(result.announcementId ?? 0)) + } + } - let (noticeBefore, noticeDate) = (newAnnouncement.notifications ?? []) + // MARK: Private method + private func generateAutoEndDate() -> Date { + Calendar.current.date(byAdding: .month, value: 1, to: Date())! + } + + private func parseNotificationDates( + from notifications: [AnnouncementRemindNotification]? + ) -> ([Date], [Date]) { + return (notifications ?? []) .reduce(into: ([Date](), [Date]())) { result, notification in switch notification { case let .before(timeInterval): @@ -59,30 +92,5 @@ public struct CreateAnnouncementUsecase { result.1.append(date) } } - - if let member = memberUserDefaultsManager.get() { - return announcementRepository - .createAnnouncement( - .init( - organizationId: newAnnouncement.organizationId, - memberId: member.id, - title: newAnnouncement.title, - content: newAnnouncement.body, - tasks: (newAnnouncement.todos ?? []).map { - .init(content: $0) - }, - endAt: newAnnouncement.endAt ?? autoGeneratedEndAt, - noticeBefore: noticeBefore, - noticeDate: noticeDate - ) - ) - .map { result in - let announcementId = Int(result.announcementId ?? 0) - return Output(announcementId: announcementId) - } - - } else { - fatalError("해당 유즈케이스는 로그인 상태에서 실행되어야합니다.") - } } } diff --git a/Projects/Domain/CommonDomainModule/CommonUsecase/Sources/UploadImageUsecase.swift b/Projects/Domain/CommonDomainModule/CommonUsecase/Sources/UploadImageUsecase.swift new file mode 100644 index 00000000..b4c25ca7 --- /dev/null +++ b/Projects/Domain/CommonDomainModule/CommonUsecase/Sources/UploadImageUsecase.swift @@ -0,0 +1,28 @@ +// +// UploadImageUsecase.swift +// CommonUsecase +// +// Created by DOYEON LEE on 8/25/24. +// + +import RxSwift + +public struct UploadImageUsecase { + // MARK: DTO + public struct Input { + public init() { } + } + + public struct Output { } + + // MARK: Dependency + + // MARK: Initializer + public init() { } + + // MARK: Execute method + public func execute(_ input: Input) -> Observable { + let outputObservable = Observable.just(Output()) + return outputObservable + } +} diff --git a/Projects/Domain/CommonDomainModule/Project.swift b/Projects/Domain/CommonDomainModule/Project.swift index 02c0d005..d78a5c14 100644 --- a/Projects/Domain/CommonDomainModule/Project.swift +++ b/Projects/Domain/CommonDomainModule/Project.swift @@ -4,5 +4,6 @@ import ProjectDescriptionHelpers let project = Project.makeDomainModule( .common, dependencies: [ + .dataInterface(.image) ] ) diff --git a/Projects/Domain/OrganizationDomainModule/OrganizationEntity/Sources/OrganizationSummaryEntity.swift b/Projects/Domain/OrganizationDomainModule/OrganizationEntity/Sources/OrganizationSummaryEntity.swift index d0d51fed..f00b9479 100644 --- a/Projects/Domain/OrganizationDomainModule/OrganizationEntity/Sources/OrganizationSummaryEntity.swift +++ b/Projects/Domain/OrganizationDomainModule/OrganizationEntity/Sources/OrganizationSummaryEntity.swift @@ -12,7 +12,7 @@ import Foundation */ public struct OrganizationSummaryEntity: Identifiable, Equatable { /// Unique identifier for the organization. - public let id: Int + public let id: Int64 /// Name of the organization. public let name: String @@ -21,7 +21,7 @@ public struct OrganizationSummaryEntity: Identifiable, Equatable { public let profileImageUrl: URL? public init( - id: Int, + id: Int64, name: String, profileImageUrl: URL? = nil ) { diff --git a/Projects/Domain/OrganizationDomainModule/OrganizationUsecase/Sources/Public/GetMyOrganizationsUsecase.swift b/Projects/Domain/OrganizationDomainModule/OrganizationUsecase/Sources/Public/GetMyOrganizationsUsecase.swift index 6b300114..96799b41 100644 --- a/Projects/Domain/OrganizationDomainModule/OrganizationUsecase/Sources/Public/GetMyOrganizationsUsecase.swift +++ b/Projects/Domain/OrganizationDomainModule/OrganizationUsecase/Sources/Public/GetMyOrganizationsUsecase.swift @@ -54,7 +54,7 @@ public class GetMyOrganizationsUsecase { let organizations: [OrganizationSummaryEntity] = result.content?.map { OrganizationSummaryEntity( - id: Int($0.organizationId), + id: $0.organizationId, name: $0.organizationName, profileImageUrl: URL(string: $0.profileImage) ) diff --git a/Projects/Present/HomePresentModule/HomePresent/Sources/AnnouncementPage/View/AnnouncementPageViewController.swift b/Projects/Present/HomePresentModule/HomePresent/Sources/AnnouncementPage/View/AnnouncementPageViewController.swift index 59abbfb0..4b40f398 100644 --- a/Projects/Present/HomePresentModule/HomePresent/Sources/AnnouncementPage/View/AnnouncementPageViewController.swift +++ b/Projects/Present/HomePresentModule/HomePresent/Sources/AnnouncementPage/View/AnnouncementPageViewController.swift @@ -54,7 +54,7 @@ class AnnouncementPageViewController: BaseViewController { Router.shared.push( .organizationDetail( .init( - id: organization.id, + id: Int64(organization.id), name: organization.name, profileImageUrl: organization.profileImageUrl ) diff --git a/Projects/Present/NewAnnouncementPresentModule/NewAnnouncementPresent/Sources/NewAnnouncementFunnel/Reactor/NewAnnouncementFunnelReactor.swift b/Projects/Present/NewAnnouncementPresentModule/NewAnnouncementPresent/Sources/NewAnnouncementFunnel/Reactor/NewAnnouncementFunnelReactor.swift index 375d137d..d1fb810f 100644 --- a/Projects/Present/NewAnnouncementPresentModule/NewAnnouncementPresent/Sources/NewAnnouncementFunnel/Reactor/NewAnnouncementFunnelReactor.swift +++ b/Projects/Present/NewAnnouncementPresentModule/NewAnnouncementPresent/Sources/NewAnnouncementFunnel/Reactor/NewAnnouncementFunnelReactor.swift @@ -125,7 +125,7 @@ class NewAnnouncementFunnelReactor: Reactor { todos: editTodoReactor.currentState.todos.map { $0.content }, - notification: Array(editNotificationReactor.currentState.selectedTimeOptions) + notifications: Array(editNotificationReactor.currentState.selectedTimeOptions) ) return self.createAnnouncementUsecase diff --git a/Projects/Present/NewOrganizationPresentModule/NewOrganizationPresent/Sources/NewOrganizationFunnel/DisplayModel/NewOrganizationFunnelPage.swift b/Projects/Present/NewOrganizationPresentModule/NewOrganizationPresent/Sources/NewOrganizationFunnel/DisplayModel/NewOrganizationFunnelPage.swift index d5d9ad15..da8a6190 100644 --- a/Projects/Present/NewOrganizationPresentModule/NewOrganizationPresent/Sources/NewOrganizationFunnel/DisplayModel/NewOrganizationFunnelPage.swift +++ b/Projects/Present/NewOrganizationPresentModule/NewOrganizationPresent/Sources/NewOrganizationFunnel/DisplayModel/NewOrganizationFunnelPage.swift @@ -10,8 +10,8 @@ import Foundation enum NewOrganizationFunnelPage: CaseIterable { case name case category -// case image -// case endDate + case image + case endDate case promotion case complete } diff --git a/Projects/Present/NewOrganizationPresentModule/NewOrganizationPresent/Sources/NewOrganizationFunnel/View/NewOrganizationFunnelView.swift b/Projects/Present/NewOrganizationPresentModule/NewOrganizationPresent/Sources/NewOrganizationFunnel/View/NewOrganizationFunnelView.swift index 65e659b8..f71c1d52 100644 --- a/Projects/Present/NewOrganizationPresentModule/NewOrganizationPresent/Sources/NewOrganizationFunnel/View/NewOrganizationFunnelView.swift +++ b/Projects/Present/NewOrganizationPresentModule/NewOrganizationPresent/Sources/NewOrganizationFunnel/View/NewOrganizationFunnelView.swift @@ -57,10 +57,10 @@ extension NewOrganizationFunnelPage: Paginable { return NewOrganizationNamePageViewController() case .category: return NewOrganizationCategoryPageViewController() -// case .image: -// return NewOrganizationImagePageViewController() -// case .endDate: -// return NewOrganizationDatePageViewController() + case .image: + return NewOrganizationImagePageViewController() + case .endDate: + return NewOrganizationDatePageViewController() case .promotion: return NewOrganizationPromotionPageViewController() case .complete: diff --git a/Projects/Present/OrganizationPresentModule/OrganizationPresent/Sources/OrganizationDetail/Reactor/OrganizationDetailReactor.swift b/Projects/Present/OrganizationPresentModule/OrganizationPresent/Sources/OrganizationDetail/Reactor/OrganizationDetailReactor.swift index b139fab2..13c6047e 100644 --- a/Projects/Present/OrganizationPresentModule/OrganizationPresent/Sources/OrganizationDetail/Reactor/OrganizationDetailReactor.swift +++ b/Projects/Present/OrganizationPresentModule/OrganizationPresent/Sources/OrganizationDetail/Reactor/OrganizationDetailReactor.swift @@ -49,7 +49,7 @@ class OrganizationDetailReactor: Reactor { switch action { case let .viewDidLoad(organization): let setOrganization = getOrganizationDetailUsecase - .execute(.init(organizationId: organization.id)) + .execute(.init(organizationId: Int(organization.id))) .map { output in Mutation.setOrganization(output.organization) } diff --git a/Projects/Present/OrganizationPresentModule/OrganizationPresent/Sources/OrganizationTab/Compositional/Converter/OrganizationTabConverter.swift b/Projects/Present/OrganizationPresentModule/OrganizationPresent/Sources/OrganizationTab/Compositional/Converter/OrganizationTabConverter.swift index 834c7868..aa473664 100644 --- a/Projects/Present/OrganizationPresentModule/OrganizationPresent/Sources/OrganizationTab/Compositional/Converter/OrganizationTabConverter.swift +++ b/Projects/Present/OrganizationPresentModule/OrganizationPresent/Sources/OrganizationTab/Compositional/Converter/OrganizationTabConverter.swift @@ -25,6 +25,7 @@ struct OrganizationTabConverter { OrganizationSection( items: entities.map { organization in OrganizationItem( + id: organization.id, name: organization.name, profileImageUrl: organization.profileImageUrl ) { diff --git a/Projects/Present/OrganizationPresentModule/OrganizationPresent/Sources/OrganizationTab/Compositional/Item/OrganizationItem.swift b/Projects/Present/OrganizationPresentModule/OrganizationPresent/Sources/OrganizationTab/Compositional/Item/OrganizationItem.swift index fb43700f..73d1f64f 100644 --- a/Projects/Present/OrganizationPresentModule/OrganizationPresent/Sources/OrganizationTab/Compositional/Item/OrganizationItem.swift +++ b/Projects/Present/OrganizationPresentModule/OrganizationPresent/Sources/OrganizationTab/Compositional/Item/OrganizationItem.swift @@ -20,6 +20,8 @@ final class OrganizationItem: CompositionalItem { let onTap: () -> Void // MARK: Data + let id: Int64 + let name: String let profileImageUrl: URL? @@ -29,10 +31,12 @@ final class OrganizationItem: CompositionalItem { // MARK: Initializer init( + id: Int64, name: String, profileImageUrl: URL?, onTap: @escaping () -> Void ) { + self.id = id self.name = name self.profileImageUrl = profileImageUrl self.onTap = onTap @@ -40,6 +44,7 @@ final class OrganizationItem: CompositionalItem { func hash(into hasher: inout Hasher) { hasher.combine(String(describing: type(of: self))) + hasher.combine(id) hasher.combine(name) } } diff --git a/Tuist/ProjectDescriptionHelpers/Module+Enums.swift b/Tuist/ProjectDescriptionHelpers/Module+Enums.swift index 66c60feb..042d6084 100644 --- a/Tuist/ProjectDescriptionHelpers/Module+Enums.swift +++ b/Tuist/ProjectDescriptionHelpers/Module+Enums.swift @@ -57,6 +57,7 @@ public enum Module { case announcement case member case todo + case image var name: String { rawValue.toUpperCamelCase() } var bundleIdenifier: String { rawValue.toBundleIdentifier() } @@ -68,6 +69,7 @@ public enum Module { case announcement case member case todo + case image var name: String { rawValue.toUpperCamelCase() } var bundleIdenifier: String { rawValue.toBundleIdentifier() }