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(