diff --git a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift index 38784bb8..6abb3e6d 100644 --- a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift +++ b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift @@ -9,6 +9,18 @@ public extension TargetDependency { } public extension TargetDependency.Feature { + static let MainFeatureTesting = TargetDependency.project( + target: ModulePaths.Feature.MainFeature.targetName(type: .testing), + path: .relativeToFeature(ModulePaths.Feature.MainFeature.rawValue) + ) + static let MainFeatureInterface = TargetDependency.project( + target: ModulePaths.Feature.MainFeature.targetName(type: .interface), + path: .relativeToFeature(ModulePaths.Feature.MainFeature.rawValue) + ) + static let MainFeature = TargetDependency.project( + target: ModulePaths.Feature.MainFeature.targetName(type: .sources), + path: .relativeToFeature(ModulePaths.Feature.MainFeature.rawValue) + ) static let TechStackAppendFeatureInterface = TargetDependency.project( target: ModulePaths.Feature.TechStackAppendFeature.targetName(type: .interface), path: .relativeToFeature(ModulePaths.Feature.TechStackAppendFeature.rawValue) diff --git a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift index 5cfc08b8..dea8f944 100644 --- a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift +++ b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift @@ -10,6 +10,7 @@ public enum ModulePaths { public extension ModulePaths { enum Feature: String { + case MainFeature case TechStackAppendFeature case StudentDetailFeature case InputInformationBaseFeature diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index 39a90a10..f09ab77f 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -46,6 +46,7 @@ let targets: [Target] = [ .Feature.InputMilitaryInfoFeature, .Feature.InputCertificateInfoFeature, .Feature.InputLanguageInfoFeature, + .Feature.MainFeature, .Feature.TechStackAppendFeature, .Feature.StudentDetailFeature, .Domain.AuthDomain, diff --git a/Projects/App/Sources/Application/DI/AppComponent.swift b/Projects/App/Sources/Application/DI/AppComponent.swift index 544c4cad..95d49391 100644 --- a/Projects/App/Sources/Application/DI/AppComponent.swift +++ b/Projects/App/Sources/Application/DI/AppComponent.swift @@ -17,6 +17,8 @@ import InputSchoolLifeInfoFeature import InputSchoolLifeInfoFeatureInterface import InputWorkInfoFeature import InputWorkInfoFeatureInterface +import MainFeature +import MainFeatureInterface import RootFeature import TechStackAppendFeatureInterface import TechStackAppendFeature @@ -81,6 +83,10 @@ final class AppComponent: BootstrapComponent { InputLanguageInfoComponent(parent: self) } + var mainBuildable: any MainBuildable { + MainComponent(parent: self) + } + var techStackAppendBuildable: any TechStackAppendBuildable { TechStackAppendComponent(parent: self) } diff --git a/Projects/App/Sources/Application/NeedleGenerated.swift b/Projects/App/Sources/Application/NeedleGenerated.swift index 69e438fc..2b32b13c 100644 --- a/Projects/App/Sources/Application/NeedleGenerated.swift +++ b/Projects/App/Sources/Application/NeedleGenerated.swift @@ -25,6 +25,8 @@ import JwtStore import JwtStoreInterface import KeychainModule import KeychainModuleInterface +import MainFeature +import MainFeatureInterface import MajorDomain import MajorDomainInterface import NeedleFoundation @@ -80,6 +82,19 @@ private class InputWorkInfoDependency74441f61366e4e5af9a2Provider: InputWorkInfo private func factoryfff86bd7854b30412216e3b0c44298fc1c149afb(_ component: NeedleFoundation.Scope) -> AnyObject { return InputWorkInfoDependency74441f61366e4e5af9a2Provider() } +private class MainDependency7c6a5b4738b211b8e155Provider: MainDependency { + var studentDomainBuildable: any StudentDomainBuildable { + return appComponent.studentDomainBuildable + } + private let appComponent: AppComponent + init(appComponent: AppComponent) { + self.appComponent = appComponent + } +} +/// ^->AppComponent->MainComponent +private func factoryc9274e46e78e70f29c54f47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { + return MainDependency7c6a5b4738b211b8e155Provider(appComponent: parent1(component) as! AppComponent) +} private class InputSchoolLifeInfoDependency30edf0903f9bdb7a60fbProvider: InputSchoolLifeInfoDependency { @@ -98,6 +113,9 @@ private class RootDependency3944cc797a4a88956fb5Provider: RootDependency { var inputInformationBuildable: any InputInformationBuildable { return appComponent.inputInformationBuildable } + var mainBuildable: any MainBuildable { + return appComponent.mainBuildable + } private let appComponent: AppComponent init(appComponent: AppComponent) { self.appComponent = appComponent @@ -331,6 +349,11 @@ extension InputWorkInfoComponent: Registration { } } +extension MainComponent: Registration { + public func registerItems() { + keyPathToName[\MainDependency.studentDomainBuildable] = "studentDomainBuildable-any StudentDomainBuildable" + } +} extension InputSchoolLifeInfoComponent: Registration { public func registerItems() { @@ -340,6 +363,7 @@ extension RootComponent: Registration { public func registerItems() { keyPathToName[\RootDependency.signinBuildable] = "signinBuildable-any SigninBuildable" keyPathToName[\RootDependency.inputInformationBuildable] = "inputInformationBuildable-any InputInformationBuildable" + keyPathToName[\RootDependency.mainBuildable] = "mainBuildable-any MainBuildable" } } extension SigninComponent: Registration { @@ -436,11 +460,12 @@ private func registerProviderFactory(_ componentPath: String, _ factory: @escapi #if !NEEDLE_DYNAMIC -@inline(never) private func register1() { +private func register1() { registerProviderFactory("^->AppComponent->JwtStoreComponent", factoryb27d5aae1eb7e73575a6f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent", factoryEmptyDependencyProvider) registerProviderFactory("^->AppComponent->KeychainComponent", factoryEmptyDependencyProvider) registerProviderFactory("^->AppComponent->InputWorkInfoComponent", factoryfff86bd7854b30412216e3b0c44298fc1c149afb) + registerProviderFactory("^->AppComponent->MainComponent", factoryc9274e46e78e70f29c54f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->InputSchoolLifeInfoComponent", factorydc1feebed8f042db375fe3b0c44298fc1c149afb) registerProviderFactory("^->AppComponent->RootComponent", factory264bfc4d4cb6b0629b40f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->SigninComponent", factory2882a056d84a613debccf47b58f8f304c97af4d5) diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Filter.imageset/Contents.json b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Filter.imageset/Contents.json new file mode 100644 index 00000000..7cbe486b --- /dev/null +++ b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Filter.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Filter.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Filter.imageset/Filter.svg b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Filter.imageset/Filter.svg new file mode 100644 index 00000000..49827c63 --- /dev/null +++ b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Filter.imageset/Filter.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/SMSLogo.imageset/Contents.json b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/SMSLogo.imageset/Contents.json new file mode 100644 index 00000000..00a1fa8d --- /dev/null +++ b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/SMSLogo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "SMSLogo.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/SMSLogo.imageset/SMSLogo.svg b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/SMSLogo.imageset/SMSLogo.svg new file mode 100644 index 00000000..1b300389 --- /dev/null +++ b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/SMSLogo.imageset/SMSLogo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Search.imageset/Contents.json b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Search.imageset/Contents.json new file mode 100644 index 00000000..668818aa --- /dev/null +++ b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Search.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Search.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Search.imageset/Search.svg b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Search.imageset/Search.svg new file mode 100644 index 00000000..fd0b1494 --- /dev/null +++ b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Search.imageset/Search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/UpArrow.imageset/Contents.json b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/UpArrow.imageset/Contents.json new file mode 100644 index 00000000..50b3e918 --- /dev/null +++ b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/UpArrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "UpArrow.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/UpArrow.imageset/UpArrow.svg b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/UpArrow.imageset/UpArrow.svg new file mode 100644 index 00000000..c34bb4f3 --- /dev/null +++ b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/UpArrow.imageset/UpArrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/Projects/Core/DesignSystem/Sources/Icon/SMSIcon.swift b/Projects/Core/DesignSystem/Sources/Icon/SMSIcon.swift index d03ffda0..a0cbfca1 100644 --- a/Projects/Core/DesignSystem/Sources/Icon/SMSIcon.swift +++ b/Projects/Core/DesignSystem/Sources/Icon/SMSIcon.swift @@ -24,14 +24,18 @@ public struct SMSIcon: View { case check case checkmark case downChevron + case filter case greenCheck case photo case plus case profile case profileSmallPlus + case search + case smsLogo case trash case leftArrow case smallPlus + case upArrow case magnifyingglass case xmark } @@ -61,6 +65,9 @@ public struct SMSIcon: View { case .downChevron: return DesignSystemAsset.Icons.downChevron.swiftUIImage + case .filter: + return DesignSystemAsset.Icons.filter.swiftUIImage + case .greenCheck: return DesignSystemAsset.Icons.greenCheck.swiftUIImage @@ -76,6 +83,12 @@ public struct SMSIcon: View { case .profileSmallPlus: return DesignSystemAsset.Icons.profileSmallPlus.swiftUIImage + case .search: + return DesignSystemAsset.Icons.search.swiftUIImage + + case .smsLogo: + return DesignSystemAsset.Icons.smsLogo.swiftUIImage + case .trash: return DesignSystemAsset.Icons.trash.swiftUIImage @@ -85,6 +98,9 @@ public struct SMSIcon: View { case .smallPlus: return DesignSystemAsset.Icons.smallPlus.swiftUIImage + case .upArrow: + return DesignSystemAsset.Icons.upArrow.swiftUIImage + case .magnifyingglass: return DesignSystemAsset.Icons.magnifyingglass.swiftUIImage diff --git a/Projects/Domain/BaseDomain/Sources/Interceptors/Jwt/JwtInterceptor.swift b/Projects/Domain/BaseDomain/Sources/Interceptors/Jwt/JwtInterceptor.swift index 6f805fd4..c301a2ef 100644 --- a/Projects/Domain/BaseDomain/Sources/Interceptors/Jwt/JwtInterceptor.swift +++ b/Projects/Domain/BaseDomain/Sources/Interceptors/Jwt/JwtInterceptor.swift @@ -28,8 +28,12 @@ public struct JwtInterceptor: InterceptorType { var newRequest = request newRequest.httpShouldHandleCookies = false let token = getToken(type: jwtType.toJwtStoreProperty) + guard !token.isEmpty else { + completion(.success(newRequest)) + return + } - newRequest.setValue(token, forHTTPHeaderField: jwtType.rawValue) + newRequest.setValue(jwtType == .accessToken ? "Bearer \(token)" : token, forHTTPHeaderField: jwtType.rawValue) if checkTokenIsExpired() { reissueToken(newRequest, jwtType: jwtType, completion: completion) } else { @@ -54,7 +58,7 @@ private extension JwtInterceptor { func getToken(type: JwtStoreProperty) -> String { switch type { case .accessToken: - return "Bearer \(jwtStore.load(property: type))" + return jwtStore.load(property: type) default: return jwtStore.load(property: type) diff --git a/Projects/Domain/FileDomain/Sources/Endpoint/FileEndpoint.swift b/Projects/Domain/FileDomain/Sources/Endpoint/FileEndpoint.swift index b78176ff..83e636d8 100644 --- a/Projects/Domain/FileDomain/Sources/Endpoint/FileEndpoint.swift +++ b/Projects/Domain/FileDomain/Sources/Endpoint/FileEndpoint.swift @@ -32,6 +32,11 @@ extension FileEndpoint: SMSEndpoint { MultiPartFormData(field: "file", data: file, fileName: fileName) ]) + case let .imageUpload(image, fileName): + return .uploadMultipart([ + MultiPartFormData(field: "file", data: image, fileName: fileName) + ]) + default: return .requestPlain } @@ -39,7 +44,7 @@ extension FileEndpoint: SMSEndpoint { var jwtTokenType: JwtTokenType { switch self { - case .dreamBookUpload: + case .dreamBookUpload, .imageUpload: return .accessToken default: @@ -47,6 +52,10 @@ extension FileEndpoint: SMSEndpoint { } } + var headers: [String: String]? { + nil + } + var errorMap: [Int: ErrorType]? { switch self { case .dreamBookUpload: diff --git a/Projects/Domain/StudentDomain/Interface/Entity/SingleStudentEntity.swift b/Projects/Domain/StudentDomain/Interface/Entity/SingleStudentEntity.swift index 3e6686b8..fcbf58da 100644 --- a/Projects/Domain/StudentDomain/Interface/Entity/SingleStudentEntity.swift +++ b/Projects/Domain/StudentDomain/Interface/Entity/SingleStudentEntity.swift @@ -1,12 +1,14 @@ import Foundation public struct SingleStudentEntity: Equatable { + public let id: String public let profileImageURL: String public let name: String public let major: String public let techStack: [String] - public init(profileImageURL: String, name: String, major: String, techStack: [String]) { + public init(id: String, profileImageURL: String, name: String, major: String, techStack: [String]) { + self.id = id self.profileImageURL = profileImageURL self.name = name self.major = major diff --git a/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentListResponseDTO.swift b/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentListResponseDTO.swift index 2804f93c..a37c8ee6 100644 --- a/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentListResponseDTO.swift +++ b/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentListResponseDTO.swift @@ -7,6 +7,7 @@ public struct FetchStudentListResponseDTO: Decodable { public let isLast: Bool public struct SingleStudentResponseDTO: Decodable { + public let id: String public let profileImg: String public let name: String public let major: String @@ -22,6 +23,7 @@ public struct FetchStudentListResponseDTO: Decodable { public extension FetchStudentListResponseDTO.SingleStudentResponseDTO { func toDomain() -> SingleStudentEntity { SingleStudentEntity( + id: id, profileImageURL: profileImg, name: name, major: major, diff --git a/Projects/Feature/InputInformationFeature/Sources/Intent/InputInformationIntent.swift b/Projects/Feature/InputInformationFeature/Sources/Intent/InputInformationIntent.swift index 65811c37..76a3b9a4 100644 --- a/Projects/Feature/InputInformationFeature/Sources/Intent/InputInformationIntent.swift +++ b/Projects/Feature/InputInformationFeature/Sources/Intent/InputInformationIntent.swift @@ -35,7 +35,10 @@ final class InputInformationIntent: InputInformationIntentProtocol { let inputSchoolLifeInfo = state.inputSchoolLifeInformationObject, let inputWorkInfo = state.inputWorkInfomationObject, let militaryServiceType = state.militaryServiceType - else { return } + else { + model?.updateIsCompleteToInputAllInfo(isComplete: false) + return + } model?.updateIsLoading(isLoading: true) Task { @@ -67,10 +70,12 @@ final class InputInformationIntent: InputInformationIntentProtocol { ) try await inputInformationUseCase.execute(req: inputInformationRequest) + inputInformationDelegate?.completeToInputInformation() model?.updateIsLoading(isLoading: false) } catch { model?.updateErrorMessage(message: error.localizedDescription) model?.updateIsLoading(isLoading: false) + model?.updateIsCompleteToInputAllInfo(isComplete: false) } } } diff --git a/Projects/Feature/InputInformationFeature/Sources/Model/InputInformationModel.swift b/Projects/Feature/InputInformationFeature/Sources/Model/InputInformationModel.swift index 764dd2b5..f447826a 100644 --- a/Projects/Feature/InputInformationFeature/Sources/Model/InputInformationModel.swift +++ b/Projects/Feature/InputInformationFeature/Sources/Model/InputInformationModel.swift @@ -15,7 +15,7 @@ final class InputInformationModel: ObservableObject, InputInformationStateProtoc var certificates: [String] = [] var militaryServiceType: MilitaryServiceType? var languages: [InputStudentInformationRequestDTO.LanguageCertificate] = [] - var isCompleteToInputAllInfo: Bool = false + @Published var isCompleteToInputAllInfo: Bool = false } extension InputInformationModel: InputInformationActionProtocol { diff --git a/Projects/Feature/InputProfileInfoFeature/Sources/Scene/InputProfileInfoView.swift b/Projects/Feature/InputProfileInfoFeature/Sources/Scene/InputProfileInfoView.swift index da1efc8e..fb25e913 100644 --- a/Projects/Feature/InputProfileInfoFeature/Sources/Scene/InputProfileInfoView.swift +++ b/Projects/Feature/InputProfileInfoFeature/Sources/Scene/InputProfileInfoView.swift @@ -73,6 +73,7 @@ struct InputProfileInfoView: View { ) { intent.majorSheetIsRequired() } + .keyboardType(.emailAddress) .focused($focusField, equals: .email) .titleWrapper("이메일") @@ -105,6 +106,7 @@ struct InputProfileInfoView: View { ) { focusField = .techStack } + .keyboardType(.URL) .focused($focusField, equals: .portfoilo) .titleWrapper("포트폴리오 URL") diff --git a/Projects/Feature/MainFeature/Interface/MainBuildable.swift b/Projects/Feature/MainFeature/Interface/MainBuildable.swift new file mode 100644 index 00000000..5a2a3674 --- /dev/null +++ b/Projects/Feature/MainFeature/Interface/MainBuildable.swift @@ -0,0 +1,6 @@ +import SwiftUI + +public protocol MainBuildable { + associatedtype ViewType: View + func makeView(delegate: any MainDelegate) -> ViewType +} diff --git a/Projects/Feature/MainFeature/Interface/MainDelegate.swift b/Projects/Feature/MainFeature/Interface/MainDelegate.swift new file mode 100644 index 00000000..7b6fd7a2 --- /dev/null +++ b/Projects/Feature/MainFeature/Interface/MainDelegate.swift @@ -0,0 +1,2 @@ +public protocol MainDelegate: AnyObject { +} diff --git a/Projects/Feature/MainFeature/Project.swift b/Projects/Feature/MainFeature/Project.swift new file mode 100644 index 00000000..f580ebbc --- /dev/null +++ b/Projects/Feature/MainFeature/Project.swift @@ -0,0 +1,13 @@ +import ProjectDescription +import ProjectDescriptionHelpers +import DependencyPlugin + +let project = Project.makeModule( + name: ModulePaths.Feature.MainFeature.rawValue, + product: .staticLibrary, + targets: [.interface, .testing, .unitTest], + internalDependencies: [ + .Feature.BaseFeature, + .Domain.StudentDomainInterface + ] +) diff --git a/Projects/Feature/MainFeature/Sources/DI/MainComponent.swift b/Projects/Feature/MainFeature/Sources/DI/MainComponent.swift new file mode 100644 index 00000000..a52bc4dd --- /dev/null +++ b/Projects/Feature/MainFeature/Sources/DI/MainComponent.swift @@ -0,0 +1,26 @@ +import SwiftUI +import MainFeatureInterface +import StudentDomainInterface +import NeedleFoundation +import BaseFeature + +public protocol MainDependency: Dependency { + var studentDomainBuildable: any StudentDomainBuildable { get } +} + +public final class MainComponent: Component, MainBuildable { + public func makeView(delegate: any MainDelegate) -> some View { + let model = MainModel() + let intent = MainIntent( + model: model, + mainDelegate: delegate, + fetchStudentListUseCase: dependency.studentDomainBuildable.fetchStudentListUseCase + ) + let container = MVIContainer( + intent: intent as MainIntentProtocol, + model: model as MainStateProtocol, + modelChangePublisher: model.objectWillChange + ) + return MainView(container: container) + } +} diff --git a/Projects/Feature/MainFeature/Sources/Intent/MainIntent.swift b/Projects/Feature/MainFeature/Sources/Intent/MainIntent.swift new file mode 100644 index 00000000..3c083e6c --- /dev/null +++ b/Projects/Feature/MainFeature/Sources/Intent/MainIntent.swift @@ -0,0 +1,30 @@ +import Combine +import MainFeatureInterface +import StudentDomainInterface + +final class MainIntent: MainIntentProtocol { + private weak var model: (any MainActionProtocol)? + private weak var mainDelegate: (any MainDelegate)? + private let fetchStudentListUseCase: any FetchStudentListUseCase + + init( + model: any MainActionProtocol, + mainDelegate: any MainDelegate, + fetchStudentListUseCase: any FetchStudentListUseCase + ) { + self.mainDelegate = mainDelegate + self.model = model + self.fetchStudentListUseCase = fetchStudentListUseCase + } + + func reachedBottom(page: Int, isLast: Bool) { + guard !isLast else { return } + Task { + let studentList = try await fetchStudentListUseCase.execute(req: .init(page: page, size: 20)) + model?.appendContent(content: studentList.studentList) + model?.updateTotalSize(totalSize: studentList.totalSize) + model?.updatePage(page: page + 1) + model?.updateIsLast(isLast: studentList.isLast) + } + } +} diff --git a/Projects/Feature/MainFeature/Sources/Intent/MainIntentProtocol.swift b/Projects/Feature/MainFeature/Sources/Intent/MainIntentProtocol.swift new file mode 100644 index 00000000..666d2cc6 --- /dev/null +++ b/Projects/Feature/MainFeature/Sources/Intent/MainIntentProtocol.swift @@ -0,0 +1,6 @@ +import Foundation +import StudentDomainInterface + +protocol MainIntentProtocol { + func reachedBottom(page: Int, isLast: Bool) +} diff --git a/Projects/Feature/MainFeature/Sources/Model/MainModel.swift b/Projects/Feature/MainFeature/Sources/Model/MainModel.swift new file mode 100644 index 00000000..a242955e --- /dev/null +++ b/Projects/Feature/MainFeature/Sources/Model/MainModel.swift @@ -0,0 +1,32 @@ +import Foundation +import StudentDomainInterface + +final class MainModel: ObservableObject, MainStateProtocol { + @Published var page: Int = 1 + @Published var totalSize: Int = 0 + @Published var isLast: Bool = false + @Published var isError: Bool = false + @Published var content: [SingleStudentEntity] = [] +} + +extension MainModel: MainActionProtocol { + func updateIsError(isError: Bool) { + self.isError = isError + } + + func updatePage(page: Int) { + self.page = page + } + + func updateTotalSize(totalSize: Int) { + self.totalSize = totalSize + } + + func updateIsLast(isLast: Bool) { + self.isLast = isLast + } + + func appendContent(content: [SingleStudentEntity]) { + self.content.append(contentsOf: content) + } +} diff --git a/Projects/Feature/MainFeature/Sources/Model/MainModelProtocol.swift b/Projects/Feature/MainFeature/Sources/Model/MainModelProtocol.swift new file mode 100644 index 00000000..a3ffadae --- /dev/null +++ b/Projects/Feature/MainFeature/Sources/Model/MainModelProtocol.swift @@ -0,0 +1,17 @@ +import StudentDomainInterface + +protocol MainStateProtocol { + var page: Int { get } + var totalSize: Int { get } + var isLast: Bool { get } + var isError: Bool { get } + var content: [SingleStudentEntity] { get } +} + +protocol MainActionProtocol: AnyObject { + func updateIsError(isError: Bool) + func updatePage(page: Int) + func updateTotalSize(totalSize: Int) + func updateIsLast(isLast: Bool) + func appendContent(content: [SingleStudentEntity]) +} diff --git a/Projects/Feature/MainFeature/Sources/Scene/MainView.swift b/Projects/Feature/MainFeature/Sources/Scene/MainView.swift new file mode 100644 index 00000000..180fee75 --- /dev/null +++ b/Projects/Feature/MainFeature/Sources/Scene/MainView.swift @@ -0,0 +1,144 @@ +import SwiftUI +import UIKit +import BaseFeature +import ViewUtil +import DesignSystem +import NukeUI + +enum MainStudentIDProperty { + static let studentScrollToTopID = "STUDENT_SCROLL_TO_TOP" +} + +struct MainView: View { + @StateObject var container: MVIContainer + var intent: any MainIntentProtocol { container.intent } + var state: any MainStateProtocol { container.model } + + var body: some View { + NavigationView { + VStack(alignment: .leading, spacing: 0) { + ScrollViewReader { proxyReader in + ScrollView(showsIndicators: false) { + HStack(spacing: 4) { + Text("\(state.totalSize)") + .smsFont(.title1, color: .system(.black)) + + Text("명") + .smsFont(.title2, color: .neutral(.n30)) + + Spacer() + } + .id(MainStudentIDProperty.studentScrollToTopID) + .padding(.top, 24) + + LazyVStack(alignment: .leading, spacing: 16) { + ForEach(state.content, id: \.id) { item in + studentListRow( + profileImageUrl: item.profileImageURL, + name: item.name, + major: item.major, + techStack: item.techStack + ) + + SMSSeparator(.neutral(.n10), height: 1) + } + + Color.clear + .onAppear { + intent.reachedBottom(page: state.page, isLast: state.isLast) + } + } + } + .overlay(alignment: .bottomTrailing) { + floatingButton { + withAnimation(.default) { + proxyReader.scrollTo(MainStudentIDProperty.studentScrollToTopID, anchor: .top) + } + } + .padding(.bottom, 40) + } + } + .padding(.horizontal, 20) + } + .navigationViewStyle(.stack) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + SMSIcon(.profile) + .clipShape(Circle()) + } + } + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + SMSIcon(.smsLogo, width: 80, height: 29) + } + } + } + } + + @ViewBuilder + func floatingButton( + action: @escaping () -> Void + ) -> some View { + ZStack(alignment: .center) { + Button { + action() + } label: { + SMSIcon(.upArrow, width: 11, height: 18) + } + } + .frame(width: 40, height: 40) + .background(Color.sms(.system(.black))) + .clipShape(Circle()) + } + + @ViewBuilder + func studentListRow( + profileImageUrl: String, + name: String, + major: String, + techStack: [String] + ) -> some View { + HStack(spacing: 12) { + LazyImage(url: URL(string: profileImageUrl)) { state in + if let image = state.image { + image + .resizable() + } else { + SMSIcon(.profile, width: 101, height: 101) + } + } + .frame(width: 101, height: 101) + .cornerRadius(8) + + VStack(alignment: .leading, spacing: 4) { + SMSText(name, font: .title2) + + SMSText(major, font: .body2) + .padding(.bottom, 16) + + techStackListView(techStack: techStack) + } + } + } + + @ViewBuilder + func techStackListView(techStack: [String]) -> some View { + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + ForEach(techStack, id: \.self) { text in + Text(text) + .smsFont(.body1, color: .neutral(.n40)) + .padding(.horizontal, 12) + .padding(.vertical, 5.5) + .background( + Color.sms(.neutral(.n10)) + ) + .cornerRadius(8) + } + + Spacer() + } + } + } +} diff --git a/Projects/Feature/MainFeature/Testing/Testing.swift b/Projects/Feature/MainFeature/Testing/Testing.swift new file mode 100644 index 00000000..b1853ce6 --- /dev/null +++ b/Projects/Feature/MainFeature/Testing/Testing.swift @@ -0,0 +1 @@ +// This is for Tuist diff --git a/Projects/Feature/MainFeature/Tests/MainFeatureTest.swift b/Projects/Feature/MainFeature/Tests/MainFeatureTest.swift new file mode 100644 index 00000000..9f6f6488 --- /dev/null +++ b/Projects/Feature/MainFeature/Tests/MainFeatureTest.swift @@ -0,0 +1,11 @@ +import XCTest + +final class MainFeatureTests: XCTestCase { + override func setUpWithError() throws {} + + override func tearDownWithError() throws {} + + func testExample() { + XCTAssertEqual(1, 1) + } +} diff --git a/Projects/Feature/RootFeature/Project.swift b/Projects/Feature/RootFeature/Project.swift index 946090b2..0a000753 100644 --- a/Projects/Feature/RootFeature/Project.swift +++ b/Projects/Feature/RootFeature/Project.swift @@ -9,6 +9,7 @@ let project = Project.makeModule( internalDependencies: [ .Feature.SigninFeatureInterface, .Feature.InputInformationFeatureInterface, + .Feature.MainFeatureInterface, .Feature.BaseFeature ] ) diff --git a/Projects/Feature/RootFeature/Sources/DI/RootComponent.swift b/Projects/Feature/RootFeature/Sources/DI/RootComponent.swift index 8a08cc4f..70f89d3c 100644 --- a/Projects/Feature/RootFeature/Sources/DI/RootComponent.swift +++ b/Projects/Feature/RootFeature/Sources/DI/RootComponent.swift @@ -3,10 +3,12 @@ import NeedleFoundation import SwiftUI import SigninFeatureInterface import InputInformationFeatureInterface +import MainFeatureInterface public protocol RootDependency: Dependency { var signinBuildable: any SigninBuildable { get } var inputInformationBuildable: any InputInformationBuildable { get } + var mainBuildable: any MainBuildable { get } } public final class RootComponent: Component { @@ -21,6 +23,7 @@ public final class RootComponent: Component { return RootView( signinBuildable: dependency.signinBuildable, inputInformationBuildable: dependency.inputInformationBuildable, + mainBuildable: dependency.mainBuildable, container: container ) } diff --git a/Projects/Feature/RootFeature/Sources/Enums/RootSceneType.swift b/Projects/Feature/RootFeature/Sources/Enums/RootSceneType.swift index 82869220..b92cd7b4 100644 --- a/Projects/Feature/RootFeature/Sources/Enums/RootSceneType.swift +++ b/Projects/Feature/RootFeature/Sources/Enums/RootSceneType.swift @@ -5,5 +5,5 @@ enum RootSceneType { case splash case signin case inputInformation - case home + case main } diff --git a/Projects/Feature/RootFeature/Sources/Intent/RootIntent.swift b/Projects/Feature/RootFeature/Sources/Intent/RootIntent.swift index 76d58b18..e1919012 100644 --- a/Projects/Feature/RootFeature/Sources/Intent/RootIntent.swift +++ b/Projects/Feature/RootFeature/Sources/Intent/RootIntent.swift @@ -1,6 +1,7 @@ import Foundation import InputInformationFeatureInterface import SigninFeatureInterface +import MainFeatureInterface final class RootIntent: RootIntentProtocol { private weak var model: (any RootActionProtocol)? @@ -12,16 +13,19 @@ final class RootIntent: RootIntentProtocol { extension RootIntent: InputInformationDelegate { func completeToInputInformation() { - model?.updateSceneType(type: .home) + model?.updateSceneType(type: .main) } } extension RootIntent: SigninDelegate { func successToSignin(isAlreadySignUp: Bool) { - model?.updateSceneType(type: isAlreadySignUp ? .home : .inputInformation) + model?.updateSceneType(type: isAlreadySignUp ? .main : .inputInformation) } func guestSignin() { - model?.updateSceneType(type: .home) + model?.updateSceneType(type: .main) } } + +extension RootIntent: MainDelegate { +} diff --git a/Projects/Feature/RootFeature/Sources/Intent/RootIntentProtocol.swift b/Projects/Feature/RootFeature/Sources/Intent/RootIntentProtocol.swift index 01cc2c13..9155037f 100644 --- a/Projects/Feature/RootFeature/Sources/Intent/RootIntentProtocol.swift +++ b/Projects/Feature/RootFeature/Sources/Intent/RootIntentProtocol.swift @@ -1,5 +1,6 @@ import Foundation import InputInformationFeatureInterface import SigninFeatureInterface +import MainFeatureInterface -protocol RootIntentProtocol: InputInformationDelegate, SigninDelegate {} +protocol RootIntentProtocol: InputInformationDelegate, SigninDelegate, MainDelegate {} diff --git a/Projects/Feature/RootFeature/Sources/Scene/RootView.swift b/Projects/Feature/RootFeature/Sources/Scene/RootView.swift index 441fd9d4..a625102b 100644 --- a/Projects/Feature/RootFeature/Sources/Scene/RootView.swift +++ b/Projects/Feature/RootFeature/Sources/Scene/RootView.swift @@ -1,6 +1,7 @@ import BaseFeature import InputInformationFeatureInterface import SigninFeatureInterface +import MainFeatureInterface import SwiftUI import ViewUtil @@ -11,14 +12,17 @@ struct RootView: View { private let signinBuildable: any SigninBuildable private let inputInformationBuildable: any InputInformationBuildable + private let mainBuildable: any MainBuildable init( signinBuildable: any SigninBuildable, inputInformationBuildable: any InputInformationBuildable, + mainBuildable: any MainBuildable, container: MVIContainer ) { self.signinBuildable = signinBuildable self.inputInformationBuildable = inputInformationBuildable + self.mainBuildable = mainBuildable self._container = StateObject(wrappedValue: container) } @@ -28,8 +32,9 @@ struct RootView: View { case .splash: Text("Splash") - case .home: - Text("Home") + case .main: + mainBuildable.makeView(delegate: intent) + .eraseToAnyView() case .signin: signinBuildable.makeView(delegate: intent) diff --git a/Tuist/Dependencies.swift b/Tuist/Dependencies.swift index a54dad75..c50de017 100644 --- a/Tuist/Dependencies.swift +++ b/Tuist/Dependencies.swift @@ -10,7 +10,7 @@ let dependencies = Dependencies( .remote(url: "https://github.com/GSM-MSG/GAuthSignin-Swift", requirement: .exact("0.0.3")), .remote(url: "https://github.com/Quick/Nimble.git", requirement: .exact("11.2.2")), .remote(url: "https://github.com/Quick/Quick.git", requirement: .exact("6.1.0")), - .remote(url: "https://github.com/GSM-MSG/Emdpoint.git", requirement: .exact("3.2.8")), + .remote(url: "https://github.com/GSM-MSG/Emdpoint.git", requirement: .exact("3.2.11")), ], baseSettings: .settings(