diff --git a/Core/Utils/Base/BaseCollectionViewCell.swift b/Core/Utils/Base/BaseCollectionViewCell.swift new file mode 100644 index 00000000..2407da8f --- /dev/null +++ b/Core/Utils/Base/BaseCollectionViewCell.swift @@ -0,0 +1,58 @@ +// +// BaseCollectionViewCell.swift +// Utils +// +// Created by kimchansoo on 2023/07/23. +// + +import UIKit + +import RxSwift +import RxCocoa + +open class BaseCollectionViewCell: UICollectionViewCell { + + // MARK: - UI + public let flexRootView = UIView() + + // MARK: - Properties + public var disposebag = DisposeBag() + + // MARK: - Methods + public override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = .white + configureAttributes() + configureHierarchy() + configureConstraints() + bind() + } + + open func configureHierarchy() { +// self.addSubview(flexRootView) + self.contentView.addSubview(flexRootView) + } + open func configureConstraints() {} + open func configureAttributes() {} + open func bind() {} + + open override func layoutSubviews() { +// self.contentView.backgroundColor = .green +// self.backgroundColor = .black +// flexRootView.backgroundColor = .blue + self.contentView.backgroundColor = .clear + self.backgroundColor = .clear + flexRootView.backgroundColor = .clear + + flexRootView.frame = contentView.bounds + flexRootView.pin.all() + flexRootView.flex.layout(mode: .fitContainer) + } + + @available(*, unavailable) + required public init?(coder: NSCoder) { + fatalError("init(coder:) is called.") + } +} + diff --git a/DesignSystem/Sources/Button/MOITButton.swift b/DesignSystem/Sources/Button/MOITButton.swift index a41228bf..a76f38b6 100644 --- a/DesignSystem/Sources/Button/MOITButton.swift +++ b/DesignSystem/Sources/Button/MOITButton.swift @@ -108,7 +108,7 @@ extension MOITButton { .marginHorizontal(12) .marginVertical(self.type.marginVertical) } - .width(self.type.width) +// .width(self.type.width) } } diff --git a/DesignSystem/Sources/Modal/MOITAlarmType.swift b/DesignSystem/Sources/Modal/MOITAlarmType.swift index e9d1d849..8f3c181c 100644 --- a/DesignSystem/Sources/Modal/MOITAlarmType.swift +++ b/DesignSystem/Sources/Modal/MOITAlarmType.swift @@ -37,3 +37,16 @@ public enum MOITAlarmType: Equatable { } } } + +extension MOITAlarmType: Hashable { + public func hash(into hasher: inout Hasher) { + switch self { + case .attendanceCheck(let remainSeconds): + hasher.combine(remainSeconds) + case .penalty(let amount): + hasher.combine(amount) + case .attendanceRating(let percent): + hasher.combine(percent) + } + } +} diff --git a/DesignSystem/Sources/Modal/MOITAlarmView.swift b/DesignSystem/Sources/Modal/MOITAlarmView.swift index 345d0a62..0974b2d8 100644 --- a/DesignSystem/Sources/Modal/MOITAlarmView.swift +++ b/DesignSystem/Sources/Modal/MOITAlarmView.swift @@ -59,7 +59,7 @@ public final class MOITAlarmView: UIView { public override func layoutSubviews() { super.layoutSubviews() self.flexRootView.pin.all() - self.flexRootView.flex.layout() + self.flexRootView.flex.layout(mode: .fitContainer) self.configureGradient() } } @@ -73,11 +73,11 @@ extension MOITAlarmView { self.flexRootView.flex .direction(.column) + .paddingHorizontal(16) .define { flex in flex.addItem(self.titleLabel) .marginTop(20) - .marginHorizontal(16) flex.addItem() .direction(.row) @@ -95,15 +95,15 @@ extension MOITAlarmView { .shrink(1) .aspectRatio(200/130) } - .marginHorizontal(16) .height(130) flex.addItem(self.button) - .marginHorizontal(16) +// .marginHorizontal(16) .marginBottom(19) + .width(100%) } - .width(335) - .height(260) +// .width(335) +// .height(260) } private func configureGradient() { diff --git a/DesignSystem/Sources/NavigationBar/NavigationColorType.swift b/DesignSystem/Sources/NavigationBar/NavigationColorType.swift index a35b2e54..1a822272 100644 --- a/DesignSystem/Sources/NavigationBar/NavigationColorType.swift +++ b/DesignSystem/Sources/NavigationBar/NavigationColorType.swift @@ -26,7 +26,7 @@ public enum NavigationColorType { var backgroundColor: UIColor { switch self { case .normal: - return ResourceKitAsset.Color.white.color + return .clear case .reverse: return ResourceKitAsset.Color.gray500.color } diff --git a/DesignSystem/Sources/StudyPreview/EmptyMOITView.swift b/DesignSystem/Sources/StudyPreview/EmptyMOITView.swift new file mode 100644 index 00000000..20c9a050 --- /dev/null +++ b/DesignSystem/Sources/StudyPreview/EmptyMOITView.swift @@ -0,0 +1,79 @@ +// +// EmptyMOITView.swift +// DesignSystem +// +// Created by kimchansoo on 2023/07/23. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import UIKit + +import ResourceKit + +import FlexLayout +import Kingfisher +import RxSwift +import RxGesture + +public final class EmptyMOITView: UIView { + + // MARK: - UI + private let flexRootView = UIView() + + private let noStudyLabel: UILabel = { + let label = UILabel() + label.text = "참여한 스터디가 없어요!" + label.font = ResourceKitFontFamily.p3 + label.textColor = ResourceKitAsset.Color.gray600.color + return label + }() + + private let studySuggestionLabel: UILabel = { + let label = UILabel() + label.text = "스터디를 생성하거나 참여해보세요" + label.font = ResourceKitFontFamily.p3 + label.textColor = ResourceKitAsset.Color.gray600.color + return label + }() + + // MARK: - Properties + private let disposebag = DisposeBag() + + // MARK: - Initializers + public init() { + super.init(frame: .zero) + configure() + } + + + @available (*, unavailable) + required init?(coder: NSCoder) { + fatalError("required init called") + } + + // MARK: - Lifecycle + public override func layoutSubviews() { + super.layoutSubviews() + + self.addSubview(flexRootView) + self.flexRootView.pin.all() + self.flexRootView.flex.layout() + self.flexRootView.layer.cornerRadius = 30 + self.flexRootView.clipsToBounds = true + } + + // MARK: - Methods + private func configure() { + + self.flexRootView.flex + .justifyContent(.center) + .alignItems(.center) + .backgroundColor(.white) + .define { flex in + flex.addItem(noStudyLabel) + .marginBottom(5) + flex.addItem(studySuggestionLabel) + } + + } +} diff --git a/DesignSystem/Sources/StudyPreview/MOITStudyPreview.swift b/DesignSystem/Sources/StudyPreview/MOITStudyPreview.swift index fb107f01..ad72809b 100644 --- a/DesignSystem/Sources/StudyPreview/MOITStudyPreview.swift +++ b/DesignSystem/Sources/StudyPreview/MOITStudyPreview.swift @@ -56,7 +56,7 @@ public final class MOITStudyPreview: UIView { public init( remainingDate: Int, - profileURLString: String, + profileURLString: String?, studyName: String, studyProgressDescription: String? ) { @@ -80,7 +80,7 @@ public final class MOITStudyPreview: UIView { // MARK: - Methods public func configure( remainingDate: Int, - profileURL: String, + profileURL: String?, studyName: String, studyProgressDescription: String? ) { @@ -156,7 +156,7 @@ public final class MOITStudyPreview: UIView { private func configureAttributes( remainingDate: Int, - profileURLString: String, + profileURLString: String?, studyName: String, studyProgressDescription: String? ) { @@ -166,7 +166,8 @@ public final class MOITStudyPreview: UIView { remainingDateLabel = MOITChip(type: .dueDate(date: remainingDate)) - if let url = URL(string: profileURLString) { + if let profileURLString = profileURLString, + let url = URL(string: profileURLString) { profileImageView.kf.setImage( with: url, options: [.processor(RoundCornerImageProcessor(cornerRadius: 20))] diff --git a/Features/MOITList/MOITListData/Implement/Repository/FineRepositoryImpl.swift b/Features/MOITList/MOITListData/Implement/Repository/FineRepositoryImpl.swift new file mode 100644 index 00000000..6c3999f4 --- /dev/null +++ b/Features/MOITList/MOITListData/Implement/Repository/FineRepositoryImpl.swift @@ -0,0 +1,33 @@ +// +// FineRepositoryImpl.swift +// MOITListDataImpl +// +// Created by kimchansoo on 2023/07/23. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import RxSwift + +import MOITListData +import MOITListDomain +import MOITNetwork + +final class FineRepositoryImpl: FineRepository { + + // MARK: - Properties + private let network: Network + + // MARK: - Initializers + public init(network: Network) { + self.network = network + } + + // MARK: - Lifecycle + + // MARK: - Methods + func fetchFine(moitId: Int) -> Single { + fatalError() + } +} diff --git a/Features/MOITList/MOITListData/Implement/Repository/MOITRepositoryImpl.swift b/Features/MOITList/MOITListData/Implement/Repository/MOITRepositoryImpl.swift new file mode 100644 index 00000000..c444e442 --- /dev/null +++ b/Features/MOITList/MOITListData/Implement/Repository/MOITRepositoryImpl.swift @@ -0,0 +1,43 @@ +// +// MOITRepository.swift +// MOITListData +// +// Created by kimchansoo on 2023/07/16. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import RxSwift + +import MOITListData +import MOITListDomain +import MOITNetwork + +public final class MOITRepositoryImpl: MOITRepository { + + + // MARK: - UI + + // MARK: - Properties + private let network: Network + + // MARK: - Initializers + public init(network: Network) { + self.network = network + } + + // MARK: - Lifecycle + + // MARK: - Methods + public func fetchMOITList() -> Single<[MOIT]> { + network.request( + with: FetchMOITListRequestable() + ) + .map { $0.moits.map { $0.toMOIT() } } + } + + public func deleteMoit(id: Int) -> Single { + fatalError() + } +} diff --git a/Features/MOITList/MOITListData/Implement/Requestable/FetchMOITListRequestable.swift b/Features/MOITList/MOITListData/Implement/Requestable/FetchMOITListRequestable.swift new file mode 100644 index 00000000..ad5413c8 --- /dev/null +++ b/Features/MOITList/MOITListData/Implement/Requestable/FetchMOITListRequestable.swift @@ -0,0 +1,44 @@ +// +// MOITRequestable.swift +// MOITListData +// +// Created by kimchansoo on 2023/07/23. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import MOITNetwork +import MOITListData + +struct FetchMOITListRequestable: Requestable { + + public typealias Response = MOITListDTO + + // MARK: - Properties + + public var baseURL: URL? { + return URL(string: "http://moit-backend-eb-env.eba-qtcnkjjy.ap-northeast-2.elasticbeanstalk.com") ?? URL(fileReferenceLiteralResourceName: "") + } + + public var path: String { + return "/api/v1/moit" + } + + public var method: HTTPMethod { + return .get + } + + public var headers: HTTPHeaders { + return [:] + } + + public var parameters: HTTPRequestParameter? { + return nil + } + + // mark: - Initializers + init() { + + } +} diff --git a/Features/MOITList/MOITListData/Interface/DTO/FineDTO.swift b/Features/MOITList/MOITListData/Interface/DTO/FineDTO.swift new file mode 100644 index 00000000..6b437b60 --- /dev/null +++ b/Features/MOITList/MOITListData/Interface/DTO/FineDTO.swift @@ -0,0 +1,57 @@ +// +// FineDTO.swift +// MOITListData +// +// Created by kimchansoo on 2023/07/23. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import MOITListDomain +// TODO: - fine 관련된 부분 나중에 이동 + +public struct FineDTO: Codable { + let id: Int + let fineAmount: Int + let userId: Int + let userNickname: String + let attendanceStatus: AttendanceStatus + let studyOrder: Int + let isApproved: Bool + let approveAt: Date + + public init( + id: Int, + fineAmount: Int, + userId: Int, + userNickname: String, + attendanceStatus: AttendanceStatus, + studyOrder: Int, + isApproved: Bool, + approveAt: Date + ) { + self.id = id + self.fineAmount = fineAmount + self.userId = userId + self.userNickname = userNickname + self.attendanceStatus = attendanceStatus + self.studyOrder = studyOrder + self.isApproved = isApproved + self.approveAt = approveAt + } + + public func toFine() -> Fine { + Fine( + id: self.id, + fineAmount: self.fineAmount, + userId: self.userId, + userNickname: self.userNickname, + attendanceStatus: self.attendanceStatus, + studyOrder: self.studyOrder, + isApproved: self.isApproved, + approveAt: self.approveAt + ) + } +} + diff --git a/Features/MOITList/MOITListData/Interface/DTO/MOITListDTO.swift b/Features/MOITList/MOITListData/Interface/DTO/MOITListDTO.swift new file mode 100644 index 00000000..54c421f9 --- /dev/null +++ b/Features/MOITList/MOITListData/Interface/DTO/MOITListDTO.swift @@ -0,0 +1,44 @@ +// +// MOITListDTO.swift +// MOITListData +// +// Created by kimchansoo on 2023/07/16. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import MOITListDomain + +public struct MOITListDTO: Decodable { + public let moits: [MOITDTO] +} + +public extension MOITListDTO { + + struct MOITDTO: Decodable { + let id: Int + let name: String + let profileUrl: String? + let isEnd: Bool + let repeatCycle: String + let dayOfWeeks: [String] + let startTime: String + let endTime: String + let dday: Int + + public func toMOIT() -> MOIT { + MOIT( + id: self.id, + name: self.name, + profileUrl: self.profileUrl, + isEnd: self.isEnd, + repeatCycle: self.repeatCycle, + dayOfWeeks: self.dayOfWeeks, + startTime: self.startTime, + endTime: self.endTime, + dday: self.dday + ) + } + } +} diff --git a/Features/MOITList/MOITListData/Interface/Repository/FineRepository.swift b/Features/MOITList/MOITListData/Interface/Repository/FineRepository.swift new file mode 100644 index 00000000..d78f5ef2 --- /dev/null +++ b/Features/MOITList/MOITListData/Interface/Repository/FineRepository.swift @@ -0,0 +1,18 @@ +// +// FineRepository.swift +// MOITListData +// +// Created by kimchansoo on 2023/07/23. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import MOITListDomain + +import RxSwift + +public protocol FineRepository { + + func fetchFine(moitId: Int) -> Single +} diff --git a/Features/MOITList/MOITListData/Interface/Repository/MOITRepository.swift b/Features/MOITList/MOITListData/Interface/Repository/MOITRepository.swift new file mode 100644 index 00000000..038ff164 --- /dev/null +++ b/Features/MOITList/MOITListData/Interface/Repository/MOITRepository.swift @@ -0,0 +1,19 @@ +// +// MOITRepository.swift +// MOITListData +// +// Created by kimchansoo on 2023/07/16. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import MOITListDomain + +import RxSwift + +public protocol MOITRepository { + + func fetchMOITList() -> Single<[MOIT]> + func deleteMoit(id: Int) -> Single +} diff --git a/Features/MOITList/MOITListData/Project.swift b/Features/MOITList/MOITListData/Project.swift new file mode 100644 index 00000000..21dff9e4 --- /dev/null +++ b/Features/MOITList/MOITListData/Project.swift @@ -0,0 +1,27 @@ +// +// MOITListAppDelegate.swift +// +// MOITList +// +// Created by kimchansoo +// + +import ProjectDescription +import ProjectDescriptionHelpers +import UtilityPlugin + +let project = Project.invertedDualTargetProject( + name: "MOITListData", + platform: .iOS, + iOSTargetVersion: "16.0.0", + interfaceDependencies: [ + .ThirdParty.RxSwift, + + .Feature.MOITList.Domain.Interface, + .MOITNetwork.Interface, + ], + implementDependencies: [ + .ThirdParty.RxSwift, + ] +) + diff --git a/Features/MOITList/MOITListData/Tests/dummy.swift b/Features/MOITList/MOITListData/Tests/dummy.swift new file mode 100644 index 00000000..9997fb44 --- /dev/null +++ b/Features/MOITList/MOITListData/Tests/dummy.swift @@ -0,0 +1,4 @@ +// +// dummy.swift +// + diff --git a/Features/MOITList/MOITListDomain/Implement/FetchMoitListUseCaseImpl.swift b/Features/MOITList/MOITListDomain/Implement/FetchMoitListUseCaseImpl.swift new file mode 100644 index 00000000..3d0c71ff --- /dev/null +++ b/Features/MOITList/MOITListDomain/Implement/FetchMoitListUseCaseImpl.swift @@ -0,0 +1,32 @@ +// +// FetchMoitListUseCaseImpl.swift +// MOITListDomain +// +// Created by kimchansoo on 2023/07/16. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import RxSwift + +import MOITListData +import MOITListDomain + +public final class FetchMoitListUseCaseImpl: FetchMoitListUseCase { + + // MARK: - Properties + private let moitRepository: MOITRepository + + // MARK: - Initializers + public init( + moitRepository: MOITRepository + ) { + self.moitRepository = moitRepository + } + + // MARK: - Methods + public func execute() -> Single<[MOIT]> { + moitRepository.fetchMOITList() + } +} diff --git a/Features/MOITList/MOITListDomain/Interface/Model/Fine.swift b/Features/MOITList/MOITListDomain/Interface/Model/Fine.swift new file mode 100644 index 00000000..c7297580 --- /dev/null +++ b/Features/MOITList/MOITListDomain/Interface/Model/Fine.swift @@ -0,0 +1,44 @@ +// +// Fine.swift +// MOITListDomain +// +// Created by kimchansoo on 2023/07/23. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +public struct Fine { + let id: Int + let fineAmount: Int + let userId: Int + let userNickname: String + let attendanceStatus: AttendanceStatus + let studyOrder: Int + let isApproved: Bool + let approveAt: Date + + public init( + id: Int, + fineAmount: Int, + userId: Int, + userNickname: String, + attendanceStatus: AttendanceStatus, + studyOrder: Int, + isApproved: Bool, + approveAt: Date + ) { + self.id = id + self.fineAmount = fineAmount + self.userId = userId + self.userNickname = userNickname + self.attendanceStatus = attendanceStatus + self.studyOrder = studyOrder + self.isApproved = isApproved + self.approveAt = approveAt + } +} + +public enum AttendanceStatus: Codable { + case late, absence +} diff --git a/Features/MOITList/MOITListDomain/Interface/Model/MOIT.swift b/Features/MOITList/MOITListDomain/Interface/Model/MOIT.swift new file mode 100644 index 00000000..d521e25e --- /dev/null +++ b/Features/MOITList/MOITListDomain/Interface/Model/MOIT.swift @@ -0,0 +1,43 @@ +// +// MOIT.swift +// MOITListDomain +// +// Created by kimchansoo on 2023/07/16. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +public struct MOIT { + public let id: Int + public let name: String + public let profileUrl: String? + public let isEnd: Bool + public let repeatCycle: String + public let dayOfWeeks: [String] + public let startTime: String + public let endTime: String + public let dday: Int + + public init( + id: Int, + name: String, + profileUrl: String?, + isEnd: Bool, + repeatCycle: String, + dayOfWeeks: [String], + startTime: String, + endTime: String, + dday: Int + ) { + self.id = id + self.name = name + self.profileUrl = profileUrl + self.isEnd = isEnd + self.repeatCycle = repeatCycle + self.dayOfWeeks = dayOfWeeks + self.startTime = startTime + self.endTime = endTime + self.dday = dday + } +} diff --git a/Features/MOITList/MOITListDomain/Interface/UseCase/DeleteMOITUseCase.swift b/Features/MOITList/MOITListDomain/Interface/UseCase/DeleteMOITUseCase.swift new file mode 100644 index 00000000..b1d4eec5 --- /dev/null +++ b/Features/MOITList/MOITListDomain/Interface/UseCase/DeleteMOITUseCase.swift @@ -0,0 +1,16 @@ +// +// DeleteMOITUseCase.swift +// MOITListDomain +// +// Created by kimchansoo on 2023/07/23. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import RxSwift + +public protocol DeleteMOITUseCase { + + func execute(moitId: Int) -> Observable +} diff --git a/Features/MOITList/MOITListDomain/Interface/UseCase/FetchLeftTimeUseCase.swift b/Features/MOITList/MOITListDomain/Interface/UseCase/FetchLeftTimeUseCase.swift new file mode 100644 index 00000000..08467e61 --- /dev/null +++ b/Features/MOITList/MOITListDomain/Interface/UseCase/FetchLeftTimeUseCase.swift @@ -0,0 +1,19 @@ +// +// FetchLeftTimeUseCase.swift +// MOITListDomain +// +// Created by kimchansoo on 2023/07/23. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import RxSwift + +/// 오늘 진행하는 스터디 중에 가장 빨리 시작하는 moit의 이름과 남은 시간 가져온다. +public protocol FetchLeftTimeUseCase { + /// 출석 10분 전부터 + /// 이후면 지각까지 얼마나 남았는지 + /// 지각 이후면 결석까지 얼마나 남았는지 + func execute(moitList: [MOIT]) -> (moitName: String, time: Date) +} diff --git a/Features/MOITList/MOITListDomain/Interface/UseCase/FetchMoitListUseCase.swift b/Features/MOITList/MOITListDomain/Interface/UseCase/FetchMoitListUseCase.swift new file mode 100644 index 00000000..af7b68e7 --- /dev/null +++ b/Features/MOITList/MOITListDomain/Interface/UseCase/FetchMoitListUseCase.swift @@ -0,0 +1,15 @@ +// +// FetchMoitListUseCase.swift +// MOITListDomain +// +// Created by kimchansoo on 2023/07/16. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import RxSwift + +public protocol FetchMoitListUseCase { + func execute() -> Single<[MOIT]> +} diff --git a/Features/MOITList/MOITListDomain/Interface/UseCase/FetchPenaltyToBePaidUseCase.swift b/Features/MOITList/MOITListDomain/Interface/UseCase/FetchPenaltyToBePaidUseCase.swift new file mode 100644 index 00000000..eef046b5 --- /dev/null +++ b/Features/MOITList/MOITListDomain/Interface/UseCase/FetchPenaltyToBePaidUseCase.swift @@ -0,0 +1,17 @@ +// +// FetchPenaltyToBePaidUseCase.swift +// MOITListDomainImpl +// +// Created by kimchansoo on 2023/07/23. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import RxSwift + +/// 벌금리스트에서 아직 안 낸 벌금들 받아와서 합해서 보여주는 usecase +public protocol FetchPenaltyToBePaidUseCase { + + func execute() -> Single +} diff --git a/Features/MOITList/MOITListDomain/Project.swift b/Features/MOITList/MOITListDomain/Project.swift new file mode 100644 index 00000000..7ca34eae --- /dev/null +++ b/Features/MOITList/MOITListDomain/Project.swift @@ -0,0 +1,27 @@ +// +// MOITListAppDelegate.swift +// +// MOITList +// +// Created by kimchansoo +// + +import ProjectDescription +import ProjectDescriptionHelpers +import UtilityPlugin + +let project = Project.invertedDualTargetProject( + name: "MOITListDomain", + platform: .iOS, + iOSTargetVersion: "16.0.0", + interfaceDependencies: [ + .ThirdParty.RxSwift, + + ], + implementDependencies: [ + .ThirdParty.RxSwift, + + .Feature.MOITList.Data.Interface, + ] +) + diff --git a/Features/MOITList/MOITListDomain/Tests/dummy.swift b/Features/MOITList/MOITListDomain/Tests/dummy.swift new file mode 100644 index 00000000..9997fb44 --- /dev/null +++ b/Features/MOITList/MOITListDomain/Tests/dummy.swift @@ -0,0 +1,4 @@ +// +// dummy.swift +// + diff --git a/Features/MOITList/MOITListUserInterface/DemoApp/Resources/LaunchScreen.storyboard b/Features/MOITList/MOITListUserInterface/DemoApp/Resources/LaunchScreen.storyboard new file mode 100644 index 00000000..eae8f562 --- /dev/null +++ b/Features/MOITList/MOITListUserInterface/DemoApp/Resources/LaunchScreen.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Features/MOITList/MOITListUserInterface/DemoApp/Sources/MOITListUserInterfaceAppDelegate.swift b/Features/MOITList/MOITListUserInterface/DemoApp/Sources/MOITListUserInterfaceAppDelegate.swift new file mode 100644 index 00000000..65a8b175 --- /dev/null +++ b/Features/MOITList/MOITListUserInterface/DemoApp/Sources/MOITListUserInterfaceAppDelegate.swift @@ -0,0 +1,111 @@ +// +// MOITListAppDelegate.swift +// +// MOITList +// +// Created by kimchansoo on . +// + +import UIKit + +import MOITListUserInterface +import MOITListUserInterfaceImpl +import MOITListDomain +import MOITListDomainImpl +import MOITListData +import MOITListDataImpl + +import RxSwift +import RIBs + +@main +final class MOITListAppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + private var router: ViewableRouting? + let dependency = MockMoitListComponent() + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + let window = UIWindow(frame: UIScreen.main.bounds) + + self.router = MOITListBuilder(dependency: dependency) + .build(withListener: MOCKMOITListListener()) + self.router?.load() + self.router?.interactable.activate() + window.rootViewController = self.router?.viewControllable.uiviewController + window.makeKeyAndVisible() + self.window = window + return true + } +} + +extension MOITListAppDelegate { + + final class MockMoitListComponent: Component, + MOITListDependency + { + var fetchLeftTimeUseCase: FetchLeftTimeUseCase + + var fetchPaneltyToBePaiedUseCase: FetchPenaltyToBePaidUseCase + + var fetchMOITListsUseCase: FetchMoitListUseCase + + init() { + self.fetchMOITListsUseCase = FetchMoitListUseCaseImpl(moitRepository: MockMoitRepository()) + self.fetchLeftTimeUseCase = MockFetchLeftTimeUseCase() + self.fetchPaneltyToBePaiedUseCase = MockFetchPenaltyToBePaidUseCase() + super.init(dependency: EmptyComponent()) + } + } + + private final class MOCKMOITListListener: MOITListListener { + } + + private final class MockMoitRepository: MOITRepository { + + func fetchMOITList() -> Single<[MOIT]> { + return Single.just([ + MOIT( + id: 1, + name: "hi", + profileUrl: "hi", + isEnd: true, + repeatCycle: "hi", + dayOfWeeks: ["hi"], + startTime: "hi", + endTime: "hi", + dday: 1 + ), + MOIT( + id: 1, + name: "hi", + profileUrl: "hi", + isEnd: true, + repeatCycle: "hi", + dayOfWeeks: ["hi"], + startTime: "hi", + endTime: "hi", + dday: 1 + ) + ]) + } + + func deleteMoit(id: Int) -> Single { + fatalError() + } + } + + private final class MockFetchPenaltyToBePaidUseCase: FetchPenaltyToBePaidUseCase { + func execute() -> Single { + return Single.just(1000) + } + } + + private final class MockFetchLeftTimeUseCase: FetchLeftTimeUseCase { + func execute(moitList: [MOIT]) -> (moitName: String, time: Date) { + return ("전전자군단", Date()) + } + } + + +} diff --git a/Features/MOITList/MOITListUserInterface/Implement/MOITListBuilder.swift b/Features/MOITList/MOITListUserInterface/Implement/MOITListBuilder.swift new file mode 100644 index 00000000..23da36b7 --- /dev/null +++ b/Features/MOITList/MOITListUserInterface/Implement/MOITListBuilder.swift @@ -0,0 +1,43 @@ +// +// MOITListBuilder.swift +// MOITListUserInterfaceImpl +// +// Created by kimchansoo on 2023/06/25. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import MOITListUserInterface +import MOITListDomain + +import RIBs + +final class MOITListComponent: Component, + MOITListInteractorDependency +{ + + var fetchLeftTimeUseCase: FetchLeftTimeUseCase { dependency.fetchLeftTimeUseCase } + + var fetchPaneltyToBePaiedUSeCase: FetchPenaltyToBePaidUseCase { dependency.fetchPaneltyToBePaiedUseCase } + + var fetchMOITListsUseCase: FetchMoitListUseCase { dependency.fetchMOITListsUseCase } +} + +// MARK: - Builder + +public final class MOITListBuilder: Builder, MOITListBuildable { + + public override init(dependency: MOITListDependency) { + super.init(dependency: dependency) + } + + public func build(withListener listener: MOITListListener) -> ViewableRouting { + let component = MOITListComponent(dependency: dependency) + let viewController = MOITListViewController() + let interactor = MOITListInteractor( + presenter: viewController, + dependency: component + ) + interactor.listener = listener + return MOITListRouter(interactor: interactor, viewController: viewController) + } +} diff --git a/Features/MOITList/MOITListUserInterface/Implement/MOITListInteractor.swift b/Features/MOITList/MOITListUserInterface/Implement/MOITListInteractor.swift new file mode 100644 index 00000000..74d72d66 --- /dev/null +++ b/Features/MOITList/MOITListUserInterface/Implement/MOITListInteractor.swift @@ -0,0 +1,208 @@ +// +// MOITListInteractor.swift +// MOITListUserInterfaceImpl +// +// Created by kimchansoo on 2023/06/25. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import DesignSystem +import MOITListUserInterface +import MOITListDomain + +import RIBs +import RxSwift +import RxRelay + +protocol MOITListRouting: ViewableRouting { + // TODO: Declare methods the interactor can invoke to manage sub-tree via the router. +} + +protocol MOITListPresentable: Presentable { + var listener: MOITListPresentableListener? { get set } + + func didReceiveMOITList(moitList: [MOITPreviewViewModel]) // MOITList 받아오는 + func didReceiveAlarm(alarms: [AlarmViewModel]) +} + +protocol MOITListInteractorDependency { + var fetchMOITListsUseCase: FetchMoitListUseCase { get } + var fetchLeftTimeUseCase: FetchLeftTimeUseCase { get } + var fetchPaneltyToBePaiedUSeCase: FetchPenaltyToBePaidUseCase { get } +// var deleteMOITUseCase: DeleteMOITUseCase { get } +} + +final class MOITListInteractor: PresentableInteractor, MOITListInteractable { + + // MARK: - Properties + + weak var router: MOITListRouting? + weak var listener: MOITListListener? + + private let dependency: MOITListInteractorDependency + + private let selectedMoitIndex = PublishRelay() + private let deleteMoitIndex = PublishRelay() + private let selectedAlarmIndex = PublishRelay() + private let createButtonTapped = PublishRelay() + private let participateButtonTapped = PublishRelay() + + // MARK: - Initializers + + public init( + presenter: MOITListPresentable, + dependency: MOITListInteractorDependency + ) { + self.dependency = dependency + super.init(presenter: presenter) + presenter.listener = self + } + + // MARK: - Methods + + override func didBecomeActive() { + super.didBecomeActive() + + bind() + } + + override func willResignActive() { + super.willResignActive() + } + + private func bind() { + let moitList = dependency.fetchMOITListsUseCase.execute() + + // moitlist 보내주기 + moitList + .subscribe(onSuccess: { [weak self] moitList in + print("fetchMOITListsUseCase list: \(moitList)") + + let moitPreviewList = moitList.compactMap { self?.makeMoitPreview(with: $0)} + self?.presenter.didReceiveMOITList(moitList: moitPreviewList) + }) + .disposeOnDeactivate(interactor: self) + + // 알람 설정 + let fine = dependency.fetchPaneltyToBePaiedUSeCase.execute() + .map { + AlarmViewModel( + alarmType: .penalty(amount: $0.description), + studyName: "" + ) + } + .asObservable() + + let alertMoitInfo = moitList + .compactMap { [weak self] moits in + self?.dependency.fetchLeftTimeUseCase.execute(moitList: moits) + } + .map { moitName, date in + AlarmViewModel( + alarmType: .attendanceCheck( + remainSeconds: Int(date.description) ?? 0 + ), + studyName: moitName + ) + } + .asObservable() + + let alarmList = Observable.merge(fine, alertMoitInfo).toArray() + + alarmList + .subscribe(onSuccess: { [weak self] alarms in + self?.presenter.didReceiveAlarm(alarms: alarms) + }) + .disposeOnDeactivate(interactor: self) + + // moit 삭제 + deleteMoitIndex + .withLatestFrom(moitList) { index, moits in + moits[index] + } + .withUnretained(self) +// .flatMap { owner, deleteMoit in +// print("deleteMoit: \(deleteMoit)") +//// return owner.dependency.deleteMOITUseCase.execute(moitId: deleteMoit.id) +// +// } + .subscribe(onNext: { owner, deleteMoit in + print("성공") + }, onError: { _ in + print("실패") + }) + .disposeOnDeactivate(interactor: self) + + // 선택된 moitdetail로 보내기 + selectedMoitIndex + .withLatestFrom(moitList) { index, moits in + moits[index] + } + .subscribe(onNext: { selectedMoit in + // TODO: - MOITDetail로 보내기 + print("selectedMoit: \(selectedMoit)") + }) + .disposeOnDeactivate(interactor: self) + + // 알람 탭 + selectedAlarmIndex + .withLatestFrom(alarmList) { index, alarmList in + alarmList[index] + } + .subscribe(onNext: { alarmViewModel in + // TODO: - alarm 타입에 따라서 벌금, 출첵으로 나눠서 보내기 + print("alarmViewModel: \(alarmViewModel)") + }) + .disposeOnDeactivate(interactor: self) + + // 생성하기로 보내기 + createButtonTapped + .withUnretained(self) + .subscribe(onNext: { owner, _ in + // TODO: - 생성하기로 보내기 + }) + .disposeOnDeactivate(interactor: self) + + // 참여하기로 보내기 + participateButtonTapped + .withUnretained(self) + .subscribe(onNext: { owner, _ in + // TODO: - 참여하기로 보내기 + }) + .disposeOnDeactivate(interactor: self) + } + + private func makeMoitPreview(with moit: MOIT) -> MOITPreviewViewModel { + let description = "\(moit.repeatCycle)마다 \(moit.dayOfWeeks) \(moit.startTime) - \( moit.endTime)" + return MOITPreviewViewModel( + remainingDate: moit.dday, + profileUrlString: moit.profileUrl, + studyName: moit.name, + studyProgressDescription: description + ) + } +} + +// MARK: - MOITListPresentableListener +extension MOITListInteractor: MOITListPresentableListener { + + func didTapDeleteMOIT(index: Int) { + deleteMoitIndex.accept(index) + } + + func didTapCreateButton() { + createButtonTapped.accept(()) + } + + func didTapParticipateButton() { + participateButtonTapped.accept(()) + } + + func didTapAlarm(index: Int) { + selectedAlarmIndex.accept(index) + } + + func didTapMOIT(index: Int) { + self.selectedMoitIndex.accept(index) + } +} diff --git a/Features/MOITList/MOITListUserInterface/Implement/MOITListRouter.swift b/Features/MOITList/MOITListUserInterface/Implement/MOITListRouter.swift new file mode 100644 index 00000000..205f0e98 --- /dev/null +++ b/Features/MOITList/MOITListUserInterface/Implement/MOITListRouter.swift @@ -0,0 +1,29 @@ +// +// MOITListRouter.swift +// MOITListUserInterfaceImpl +// +// Created by kimchansoo on 2023/06/25. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import MOITListUserInterface + +import RIBs + +protocol MOITListInteractable: Interactable { + var router: MOITListRouting? { get set } + var listener: MOITListListener? { get set } +} + +protocol MOITListViewControllable: ViewControllable { + // TODO: Declare methods the router invokes to manipulate the view hierarchy. +} + +final class MOITListRouter: ViewableRouter, MOITListRouting { + + // TODO: Constructor inject child builder protocols to allow building children. + override init(interactor: MOITListInteractable, viewController: MOITListViewControllable) { + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } +} diff --git a/Features/MOITList/MOITListUserInterface/Implement/MOITListViewController.swift b/Features/MOITList/MOITListUserInterface/Implement/MOITListViewController.swift new file mode 100644 index 00000000..573bdd19 --- /dev/null +++ b/Features/MOITList/MOITListUserInterface/Implement/MOITListViewController.swift @@ -0,0 +1,245 @@ +// +// MOITListViewController.swift +// MOITListUserInterfaceImpl +// +// Created by kimchansoo on 2023/06/25. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import DesignSystem +import MOITListUserInterface +import MOITListDomain +import Utils + +import ResourceKit +import RIBs +import RxSwift +import UIKit +import FlexLayout +import PinLayout + +protocol MOITListPresentableListener: AnyObject { + + func didTapMOIT(index: Int) // MOIT 하나 탭 시 불리는 함수 + func didTapDeleteMOIT(index: Int) // MOIT 하나 삭제 시 불리는 함수 + func didTapAlarm(index: Int) + func didTapCreateButton() + func didTapParticipateButton() +} + +final class MOITListViewController: BaseViewController, MOITListPresentable, MOITListViewControllable { + + // MARK: - UI + private let alarmRootView = UIView() + private let pagableAlarmView = PagableCollectionView(frame: .zero) + + private let listRootView = UIView() + + + private let moitTitleLabel: UILabel = { + let label = UILabel() + label.text = "전체 스터디" + label.textColor = ResourceKitAsset.Color.gray800.color + label.font = ResourceKitFontFamily.h6 + return label + }() + + private let moitCountLabel: UILabel = { + let label = UILabel() + label.text = "0" + label.textColor = ResourceKitAsset.Color.blue600.color + label.font = ResourceKitFontFamily.h6 + return label + }() + + private let emptyMOITView = EmptyMOITView() + + private var moitPreviewList: [MOITStudyPreview] = [] + + private let createButton = MOITButton( + type: .small, + title: "스터디 생성", + titleColor: ResourceKitAsset.Color.gray800.color, + backgroundColor: ResourceKitAsset.Color.gray200.color + ) + + private let participateButton = MOITButton( + type: .small, + title: "스터디 참여", + titleColor: ResourceKitAsset.Color.white.color, + backgroundColor: ResourceKitAsset.Color.blue800.color + ) + + // MARK: - Properties + weak var listener: MOITListPresentableListener? + + private lazy var moitList: [MOITList] = [] + + // MARK: - Initializers + public init(listener: MOITListPresentableListener? = nil) { + self.listener = listener + super.init() + } + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + + self.view.backgroundColor = ResourceKitAsset.Color.gray100.color + self.flexRootView.backgroundColor = ResourceKitAsset.Color.gray100.color + } + + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + configureNavigationBar( + leftItems: [.logo], + title: "", + rightItems: [ + .alarm, + .setting + ] + ) + } + + // MARK: - Methods + override func configureConstraints() { + super.configureConstraints() + + flexRootView.flex + .paddingHorizontal(20) + .paddingTop(20) + .define { flex in + // 알람 + flex.addItem(alarmRootView) + .marginBottom(30) + + // 모잇 몇개인지 + flex.addItem() + .direction(.row) + .define { flex in + flex.addItem(moitTitleLabel) + .marginRight(4) + flex.addItem(moitCountLabel) + } + .marginBottom(20) + + // 모잇 리스트 + flex.addItem(listRootView) + .define({ flex in + flex.addItem(emptyMOITView) + .height(108) + .width(100%) + }) + + // 참여, 생성 버튼 + flex.addItem() + .direction(.row) + .justifyContent(.spaceBetween) + .define { flex in + flex.addItem(createButton) + .width(47%) + flex.addItem(participateButton) + .width(47%) + } + } + } + + override func bind() { + super.bind() + + pagableAlarmView.thumbnailDidTap.asObservable() + .withUnretained(self) + .subscribe(onNext: { owner, index in + owner.listener?.didTapAlarm(index: index.row) + }) + .disposed(by: disposebag) + + + createButton.rx.tap + .withUnretained(self) + .subscribe(onNext: { owner, _ in + owner.listener?.didTapCreateButton() + }) + .disposed(by: disposebag) + + participateButton.rx.tap + .withUnretained(self) + .subscribe(onNext: { owner, _ in + owner.listener?.didTapParticipateButton() + }) + .disposed(by: disposebag) + + moitPreviewList.enumerated().forEach { index, preview in + // 탭 + preview.rx.didTap + .withUnretained(self) + .subscribe(onNext: { owner, _ in + owner.listener?.didTapMOIT(index: index) + }) + .disposed(by: disposebag) + + // 삭제 + preview.rx.didConfirmDelete + .withUnretained(self) + .subscribe(onNext: { owner, _ in + owner.listener?.didTapDeleteMOIT(index: index) + }) + .disposed(by: disposebag) + } + } + + func didReceiveMOITList(moitList: [MOITPreviewViewModel]) { + print(#function, "previewViewModel: \(moitList)") + + if moitList.isEmpty { return } + + moitCountLabel.text = moitList.count.description + moitCountLabel.flex.markDirty() + + emptyMOITView.flex.display(.none) + + // TODO: - studypreview 모아서 저장해두고 걔를 additem + let moitPreviewList = moitList.map { makeStudyPreview(with: $0) } + self.moitPreviewList = moitPreviewList + + listRootView.flex.define { flex in + moitPreviewList.forEach { + flex.addItem($0) + .height(100) + .width(100%) + .marginBottom(10) + } + } + listRootView.flex.markDirty() + listRootView.flex.layout() + flexRootView.flex.layout() + } + + func didReceiveAlarm(alarms: [AlarmViewModel]) { + + self.pagableAlarmView.configure(alarmViewModels: alarms) + + alarmRootView.flex.define { flex in + flex.addItem(pagableAlarmView) + } + + // TODO: - flexRootView.flex.layout만 해도 레이아웃 잡히는지 실 데이터 받을 때 확인 + pagableAlarmView.flex.markDirty() + alarmRootView.flex.markDirty() + flexRootView.flex.layout() + } + +} + +private extension MOITListViewController { + func makeStudyPreview(with viewModel: MOITPreviewViewModel) -> MOITStudyPreview { + MOITStudyPreview( + remainingDate: viewModel.remainingDate, + profileURLString: viewModel.profileUrlString, + studyName: viewModel.studyName, + studyProgressDescription: viewModel.studyProgressDescription + ) + } + +} diff --git a/Features/MOITList/MOITListUserInterface/Implement/View/PagableView.swift b/Features/MOITList/MOITListUserInterface/Implement/View/PagableView.swift new file mode 100644 index 00000000..5f6c1b43 --- /dev/null +++ b/Features/MOITList/MOITListUserInterface/Implement/View/PagableView.swift @@ -0,0 +1,145 @@ +// +// PagableView.swift +// MOITListUserInterface +// +// Created by kimchansoo on 2023/07/23. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import UIKit +import Foundation + +import DesignSystem +import ResourceKit +import Utils + +import FlexLayout +import PinLayout +import RxCocoa + +public final class PagableCollectionView: BaseView { + + // MARK: UI + + private lazy var layout: UICollectionViewFlowLayout = { + let layout = UICollectionViewFlowLayout() + layout.minimumLineSpacing = 0 + layout.scrollDirection = .horizontal + return layout + }() + + private lazy var collectionView: UICollectionView = { + let view = UICollectionView(frame: .zero, collectionViewLayout: layout) + view.isScrollEnabled = true + view.isPagingEnabled = true + view.clipsToBounds = true + view.alwaysBounceHorizontal = true + view.showsHorizontalScrollIndicator = false + view.showsVerticalScrollIndicator = false + view.layer.cornerRadius = 20 + return view + }() + + private lazy var thumbnailPageControl: UIPageControl = { + let control = UIPageControl() + control.tintColor = ResourceKitAsset.Color.gray600.color + control.isUserInteractionEnabled = false + control.translatesAutoresizingMaskIntoConstraints = false + return control + }() + + // MARK: Properties + var thumbnailDidTap: ControlEvent { + return collectionView.rx.itemSelected + } + + private var dataSource: DataSource? + + // MARK: Initializers + public override init(frame: CGRect) { + super.init(frame: .zero) + } + + // MARK: Methods + + public override func configureConstraints() { + super.configureConstraints() + self.backgroundColor = .clear + flexRootView.flex + .define { flex in + flex.addItem(collectionView) + .height(272) + .width(100%) + .marginBottom(20) + + flex.addItem(thumbnailPageControl) + } + } + + public override func configureAttributes() { + super.configureAttributes() + + self.configureCollectionView() + self.dataSource = generateDataSource() + } + + func configure(alarmViewModels: [AlarmViewModel]) { + thumbnailPageControl.numberOfPages = alarmViewModels.count + + let snaphot = generateSnapshot(alarmViews: alarmViewModels) + dataSource?.apply(snaphot) + } +} + +extension PagableCollectionView: UICollectionViewDelegateFlowLayout { + + enum Section { + case main + } + + typealias DataSource = UICollectionViewDiffableDataSource + + private func configureCollectionView() { + self.collectionView.register(ThumbnailCollectionViewCell.self, forCellWithReuseIdentifier: "ThumbnailCollectionViewCell") + self.collectionView.showsHorizontalScrollIndicator = false + self.collectionView.delegate = self + } + + private func generateDataSource() -> DataSource { + return DataSource(collectionView: self.collectionView) { collectionView, indexPath, itemIdentifier in + guard let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: "ThumbnailCollectionViewCell", + for: indexPath + ) as? ThumbnailCollectionViewCell + else { + return UICollectionViewCell() + } + + cell.configure( + viewType: itemIdentifier.alarmType, + studyName: itemIdentifier.studyName + ) + return cell + } + } + + private func generateSnapshot(alarmViews: [AlarmViewModel]) -> NSDiffableDataSourceSnapshot { + var snapShot = NSDiffableDataSourceSnapshot() + snapShot.appendSections([.main]) + snapShot.appendItems(alarmViews, toSection: .main) + return snapShot + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + + let width = self.frame.width + return CGSize(width: width, height: 272) + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + let page = scrollView.contentOffset.x / scrollView.frame.size.width + thumbnailPageControl.currentPage = Int(round(page)) + } +} + + diff --git a/Features/MOITList/MOITListUserInterface/Implement/View/ThumbnailCollectionViewCell.swift b/Features/MOITList/MOITListUserInterface/Implement/View/ThumbnailCollectionViewCell.swift new file mode 100644 index 00000000..3e712329 --- /dev/null +++ b/Features/MOITList/MOITListUserInterface/Implement/View/ThumbnailCollectionViewCell.swift @@ -0,0 +1,57 @@ +// +// ThumbnailCollectionViewCell.swift +// MOITListUserInterface +// +// Created by kimchansoo on 2023/07/23. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import UIKit + +import DesignSystem +import Utils + +import PinLayout +import FlexLayout + +final class ThumbnailCollectionViewCell: BaseCollectionViewCell { + + // MARK: UI + + private var alarmView: MOITAlarmView! + + // MARK: Properties + + // MARK: Initializers + init(viewType: MOITAlarmType, studyName: String) { + super.init(frame: .zero) + configure(viewType: viewType, studyName: studyName) + } + + override init(frame: CGRect) { + super.init(frame: .zero) + } + + // MARK: Methods + + override func configureConstraints() { + super.configureConstraints() + } + + public func configure(viewType: MOITAlarmType, studyName: String) { + let alarmView = MOITAlarmView(type: viewType, studyName: studyName) + self.alarmView = alarmView + + flexRootView.flex.define { flex in + flex.addItem(self.alarmView) + .width(100%) + .height(100%) + .alignSelf(.center) + .grow(1) + .layout(mode: .fitContainer) + } + + self.alarmView.flex.layout(mode: .fitContainer) + flexRootView.flex.layout(mode: .fitContainer) + } +} diff --git a/Features/MOITList/MOITListUserInterface/Implement/ViewModel/AlarmViewModel.swift b/Features/MOITList/MOITListUserInterface/Implement/ViewModel/AlarmViewModel.swift new file mode 100644 index 00000000..74370c39 --- /dev/null +++ b/Features/MOITList/MOITListUserInterface/Implement/ViewModel/AlarmViewModel.swift @@ -0,0 +1,16 @@ +// +// AlarmViewModel.swift +// MOITListUserInterfaceImpl +// +// Created by kimchansoo on 2023/07/23. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import DesignSystem + +public struct AlarmViewModel: Hashable { + let alarmType: MOITAlarmType + let studyName: String +} diff --git a/Features/MOITList/MOITListUserInterface/Implement/ViewModel/MOITPreviewViewModel.swift b/Features/MOITList/MOITListUserInterface/Implement/ViewModel/MOITPreviewViewModel.swift new file mode 100644 index 00000000..d67ce8a3 --- /dev/null +++ b/Features/MOITList/MOITListUserInterface/Implement/ViewModel/MOITPreviewViewModel.swift @@ -0,0 +1,28 @@ +// +// MOITPreviewViewModel.swift +// MOITListUserInterface +// +// Created by kimchansoo on 2023/07/16. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +public struct MOITPreviewViewModel { + let remainingDate: Int + let profileUrlString: String? + let studyName: String + let studyProgressDescription: String? + + public init( + remainingDate: Int, + profileUrlString: String?, + studyName: String, + studyProgressDescription: String? + ) { + self.remainingDate = remainingDate + self.profileUrlString = profileUrlString + self.studyName = studyName + self.studyProgressDescription = studyProgressDescription + } +} diff --git a/Features/MOITList/MOITListUserInterface/Interface/MOITListBuildable.swift b/Features/MOITList/MOITListUserInterface/Interface/MOITListBuildable.swift new file mode 100644 index 00000000..ca3f0732 --- /dev/null +++ b/Features/MOITList/MOITListUserInterface/Interface/MOITListBuildable.swift @@ -0,0 +1,13 @@ +// +// MOITListBuildable.swift +// MOITListUserInterface +// +// Created by kimchansoo on 2023/07/15. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import RIBs + +public protocol MOITListBuildable: Buildable { + func build(withListener listener: MOITListListener) -> ViewableRouting +} diff --git a/Features/MOITList/MOITListUserInterface/Interface/MOITListDependency.swift b/Features/MOITList/MOITListUserInterface/Interface/MOITListDependency.swift new file mode 100644 index 00000000..0e402b97 --- /dev/null +++ b/Features/MOITList/MOITListUserInterface/Interface/MOITListDependency.swift @@ -0,0 +1,18 @@ +// +// MOITListDependency.swift +// MOITListUserInterface +// +// Created by kimchansoo on 2023/07/15. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import RIBs + +import MOITListDomain + +public protocol MOITListDependency: Dependency { + + var fetchMOITListsUseCase: FetchMoitListUseCase { get } + var fetchLeftTimeUseCase: FetchLeftTimeUseCase { get } + var fetchPaneltyToBePaiedUseCase: FetchPenaltyToBePaidUseCase { get } +} diff --git a/Features/MOITList/MOITListUserInterface/Interface/MOITListListener.swift b/Features/MOITList/MOITListUserInterface/Interface/MOITListListener.swift new file mode 100644 index 00000000..7823139b --- /dev/null +++ b/Features/MOITList/MOITListUserInterface/Interface/MOITListListener.swift @@ -0,0 +1,13 @@ +// +// MOITListListener.swift +// MOITListUserInterface +// +// Created by kimchansoo on 2023/07/15. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +public protocol MOITListListener: AnyObject { + // TODO: Declare methods the interactor can invoke to communicate with other RIBs. +} diff --git a/Features/MOITList/MOITListUserInterface/Project.swift b/Features/MOITList/MOITListUserInterface/Project.swift new file mode 100644 index 00000000..518339df --- /dev/null +++ b/Features/MOITList/MOITListUserInterface/Project.swift @@ -0,0 +1,43 @@ +// +// MOITListAppDelegate.swift +// +// MOITList +// +// Created by kimchansoo +// + +import ProjectDescription +import ProjectDescriptionHelpers +import UtilityPlugin + +let project = Project.invertedDualTargetProjectWithDemoApp( + name: "MOITListUserInterface", + platform: .iOS, + iOSTargetVersion: "16.0.0", + interfaceDependencies: [ + .ThirdParty.RxSwift, + .ThirdParty.RIBs, + ], + implementDependencies: [ + .ThirdParty.RxSwift, + .ThirdParty.RxCocoa, + .ThirdParty.RxRelay, + .ThirdParty.RIBs, + .ThirdParty.PinLayout, + .ThirdParty.FlexLayout, + .ThirdParty.RxGesture, + + .ResourceKit, + .DesignSystem, + .Core.Utils, + + .Feature.MOITList.Domain.Interface, + ], + demoAppDependencies: [ + .Feature.MOITList.Domain.Implement, + .Feature.MOITList.Data.Implement, + + ], + isUserInterface: true +) + diff --git a/Features/MOITList/MOITListUserInterface/Tests/dummy.swift b/Features/MOITList/MOITListUserInterface/Tests/dummy.swift new file mode 100644 index 00000000..9997fb44 --- /dev/null +++ b/Features/MOITList/MOITListUserInterface/Tests/dummy.swift @@ -0,0 +1,4 @@ +// +// dummy.swift +// + diff --git a/Plugins/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift b/Plugins/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift index 29f76a34..b6b4619c 100644 --- a/Plugins/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift +++ b/Plugins/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift @@ -5,7 +5,7 @@ import ProjectDescription extension TargetDependency { public struct Feature { - public struct StudyList { + public struct MOITList { public struct Data {} public struct Domain {} public struct UserInterface {} @@ -62,8 +62,8 @@ public extension TargetDependency.Core { } // MARK: - Features/Home -public extension TargetDependency.Feature.StudyList { - static let folderName = "StudyList" +public extension TargetDependency.Feature.MOITList { + static let folderName = "MOITList" static func project(name: String, isInterface: Bool) -> TargetDependency { let postfix: String = isInterface ? "" : "Impl" return .project(target: "\(folderName)\(name)\(postfix)", @@ -71,19 +71,19 @@ public extension TargetDependency.Feature.StudyList { } } -public extension TargetDependency.Feature.StudyList.UserInterface { - static let Interface = TargetDependency.Feature.StudyList.project(name: "UserInterface", isInterface: true) - static let Implement = TargetDependency.Feature.StudyList.project(name: "UserInterface", isInterface: false) +public extension TargetDependency.Feature.MOITList.UserInterface { + static let Interface = TargetDependency.Feature.MOITList.project(name: "UserInterface", isInterface: true) + static let Implement = TargetDependency.Feature.MOITList.project(name: "UserInterface", isInterface: false) } -public extension TargetDependency.Feature.StudyList.Domain { - static let Interface = TargetDependency.Feature.StudyList.project(name: "Domain", isInterface: true) - static let Implement = TargetDependency.Feature.StudyList.project(name: "Domain", isInterface: false) +public extension TargetDependency.Feature.MOITList.Domain { + static let Interface = TargetDependency.Feature.MOITList.project(name: "Domain", isInterface: true) + static let Implement = TargetDependency.Feature.MOITList.project(name: "Domain", isInterface: false) } -public extension TargetDependency.Feature.StudyList.Data { - static let Interface = TargetDependency.Feature.StudyList.project(name: "Data", isInterface: true) - static let Implement = TargetDependency.Feature.StudyList.project(name: "Data", isInterface: false) +public extension TargetDependency.Feature.MOITList.Data { + static let Interface = TargetDependency.Feature.MOITList.project(name: "Data", isInterface: true) + static let Implement = TargetDependency.Feature.MOITList.project(name: "Data", isInterface: false) } // MARK: - Features/SignUp