diff --git a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift index f0d4422c..8af11aa6 100644 --- a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift +++ b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift @@ -17,6 +17,14 @@ public extension TargetDependency.Feature { target: ModulePaths.Feature.InputPrizeInfoFeature.targetName(type: .sources), path: .relativeToFeature(ModulePaths.Feature.InputPrizeInfoFeature.rawValue) ) + static let MyPageFeatureInterface = TargetDependency.project( + target: ModulePaths.Feature.MyPageFeature.targetName(type: .interface), + path: .relativeToFeature(ModulePaths.Feature.MyPageFeature.rawValue) + ) + static let MyPageFeature = TargetDependency.project( + target: ModulePaths.Feature.MyPageFeature.targetName(type: .sources), + path: .relativeToFeature(ModulePaths.Feature.MyPageFeature.rawValue) + ) static let InputProjectInfoFeatureInterface = TargetDependency.project( target: ModulePaths.Feature.InputProjectInfoFeature.targetName(type: .interface), path: .relativeToFeature(ModulePaths.Feature.InputProjectInfoFeature.rawValue) diff --git a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift index fbf53e4c..8881aa3c 100644 --- a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift +++ b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift @@ -11,6 +11,7 @@ public enum ModulePaths { public extension ModulePaths { enum Feature: String { case InputPrizeInfoFeature + case MyPageFeature case InputProjectInfoFeature case SplashFeature case MainFeature diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index 37cd1781..b44bd6ba 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -53,6 +53,7 @@ let targets: [Target] = [ .Feature.TechStackAppendFeature, .Feature.StudentDetailFeature, .Feature.FilterFeature, + .Feature.MyPageFeature, .Domain.AuthDomain, .Domain.StudentDomain, .Domain.FileDomain, diff --git a/Projects/App/Sources/Application/DI/AppComponent.swift b/Projects/App/Sources/Application/DI/AppComponent.swift index 382a3f78..e79fc5bb 100644 --- a/Projects/App/Sources/Application/DI/AppComponent.swift +++ b/Projects/App/Sources/Application/DI/AppComponent.swift @@ -29,6 +29,8 @@ import KeychainModule import KeychainModuleInterface import MainFeature import MainFeatureInterface +import MyPageFeature +import MyPageFeatureInterface import MajorDomain import MajorDomainInterface import NeedleFoundation @@ -102,6 +104,10 @@ final class AppComponent: BootstrapComponent { MainComponent(parent: self) } + var myPageBuildable: any MyPageBuildable { + MyPageComponent(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 ff0e9375..ea73b906 100644 --- a/Projects/App/Sources/Application/NeedleGenerated.swift +++ b/Projects/App/Sources/Application/NeedleGenerated.swift @@ -35,6 +35,8 @@ import MainFeature import MainFeatureInterface import MajorDomain import MajorDomainInterface +import MyPageFeature +import MyPageFeatureInterface import NeedleFoundation import RootFeature import SigninFeature @@ -105,6 +107,34 @@ private class InputProjectInfoDependencye065c7f60c5c520999a0Provider: InputProje private func factory2378736e5949c5e8e9f4f47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { return InputProjectInfoDependencye065c7f60c5c520999a0Provider(appComponent: parent1(component) as! AppComponent) } +private class MyPageDependency48d84b530313b3ee40feProvider: MyPageDependency { + var userDomainBuildable: any UserDomainBuildable { + return appComponent.userDomainBuildable + } + var authDomainBuildable: any AuthDomainBuildable { + return appComponent.authDomainBuildable + } + var techStackAppendBuildable: any TechStackAppendBuildable { + return appComponent.techStackAppendBuildable + } + var fileDomainBuildable: any FileDomainBuildable { + return appComponent.fileDomainBuildable + } + var studentDomainBuildable: any StudentDomainBuildable { + return appComponent.studentDomainBuildable + } + var majorDomainBuildable: any MajorDomainBuildable { + return appComponent.majorDomainBuildable + } + private let appComponent: AppComponent + init(appComponent: AppComponent) { + self.appComponent = appComponent + } +} +/// ^->AppComponent->MyPageComponent +private func factory0f6f456ebf157d02dfb3f47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { + return MyPageDependency48d84b530313b3ee40feProvider(appComponent: parent1(component) as! AppComponent) +} private class InputWorkInfoDependency74441f61366e4e5af9a2Provider: InputWorkInfoDependency { @@ -120,12 +150,12 @@ private class MainDependency7c6a5b4738b211b8e155Provider: MainDependency { var studentDomainBuildable: any StudentDomainBuildable { return appComponent.studentDomainBuildable } - var authDomainBuildable: any AuthDomainBuildable { - return appComponent.authDomainBuildable - } var filterBuildable: any FilterBuildable { return appComponent.filterBuildable } + var myPageBuildable: any MyPageBuildable { + return appComponent.myPageBuildable + } var studentDetailBuildable: any StudentDetailBuildable { return appComponent.studentDetailBuildable } @@ -444,6 +474,16 @@ extension InputProjectInfoComponent: Registration { keyPathToName[\InputProjectInfoDependency.techStackAppendBuildable] = "techStackAppendBuildable-any TechStackAppendBuildable" } } +extension MyPageComponent: Registration { + public func registerItems() { + keyPathToName[\MyPageDependency.userDomainBuildable] = "userDomainBuildable-any UserDomainBuildable" + keyPathToName[\MyPageDependency.authDomainBuildable] = "authDomainBuildable-any AuthDomainBuildable" + keyPathToName[\MyPageDependency.techStackAppendBuildable] = "techStackAppendBuildable-any TechStackAppendBuildable" + keyPathToName[\MyPageDependency.fileDomainBuildable] = "fileDomainBuildable-any FileDomainBuildable" + keyPathToName[\MyPageDependency.studentDomainBuildable] = "studentDomainBuildable-any StudentDomainBuildable" + keyPathToName[\MyPageDependency.majorDomainBuildable] = "majorDomainBuildable-any MajorDomainBuildable" + } +} extension InputWorkInfoComponent: Registration { public func registerItems() { @@ -452,8 +492,8 @@ extension InputWorkInfoComponent: Registration { extension MainComponent: Registration { public func registerItems() { keyPathToName[\MainDependency.studentDomainBuildable] = "studentDomainBuildable-any StudentDomainBuildable" - keyPathToName[\MainDependency.authDomainBuildable] = "authDomainBuildable-any AuthDomainBuildable" keyPathToName[\MainDependency.filterBuildable] = "filterBuildable-any FilterBuildable" + keyPathToName[\MainDependency.myPageBuildable] = "myPageBuildable-any MyPageBuildable" keyPathToName[\MainDependency.studentDetailBuildable] = "studentDetailBuildable-any StudentDetailBuildable" keyPathToName[\MainDependency.userDomainBuildable] = "userDomainBuildable-any UserDomainBuildable" } @@ -586,6 +626,7 @@ private func register1() { registerProviderFactory("^->AppComponent->KeychainComponent", factoryEmptyDependencyProvider) registerProviderFactory("^->AppComponent->SplashComponent", factoryace9f05f51d68f4c0677f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->InputProjectInfoComponent", factory2378736e5949c5e8e9f4f47b58f8f304c97af4d5) + registerProviderFactory("^->AppComponent->MyPageComponent", factory0f6f456ebf157d02dfb3f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->InputWorkInfoComponent", factoryfff86bd7854b30412216e3b0c44298fc1c149afb) registerProviderFactory("^->AppComponent->MainComponent", factoryc9274e46e78e70f29c54f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->InputSchoolLifeInfoComponent", factorydc1feebed8f042db375fe3b0c44298fc1c149afb) diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Camera.imageset/Camera.svg b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Camera.imageset/Camera.svg index fcbea1e6..966fc29c 100644 --- a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Camera.imageset/Camera.svg +++ b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Camera.imageset/Camera.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/RedLogout.imageset/Contents.json b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Logout.imageset/Contents.json similarity index 77% rename from Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/RedLogout.imageset/Contents.json rename to Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Logout.imageset/Contents.json index 2376ab4c..94ba0f9a 100644 --- a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/RedLogout.imageset/Contents.json +++ b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Logout.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "RedLogout.svg", + "filename" : "Logout.svg", "idiom" : "universal" } ], diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/RedLogout.imageset/RedLogout.svg b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Logout.imageset/Logout.svg similarity index 83% rename from Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/RedLogout.imageset/RedLogout.svg rename to Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Logout.imageset/Logout.svg index 026de5e6..62a0eabd 100644 --- a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/RedLogout.imageset/RedLogout.svg +++ b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Logout.imageset/Logout.svg @@ -1,4 +1,4 @@ - + diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/LogoutLine.imageset/Contents.json b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/LogoutLine.imageset/Contents.json new file mode 100644 index 00000000..0e798e55 --- /dev/null +++ b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/LogoutLine.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "LogoutLine.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/LogoutLine.imageset/LogoutLine.svg b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/LogoutLine.imageset/LogoutLine.svg new file mode 100644 index 00000000..e5514b25 --- /dev/null +++ b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/LogoutLine.imageset/LogoutLine.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Photo.imageset/Contents.json b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Photo.imageset/Contents.json index d2b87ab2..52ca28db 100644 --- a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Photo.imageset/Contents.json +++ b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Photo.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Image.svg", + "filename" : "icon.svg", "idiom" : "universal" } ], diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Photo.imageset/Image.svg b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Photo.imageset/Image.svg deleted file mode 100644 index 88ab692b..00000000 --- a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Photo.imageset/Image.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Photo.imageset/icon.svg b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Photo.imageset/icon.svg new file mode 100644 index 00000000..7817b176 --- /dev/null +++ b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Photo.imageset/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Projects/Feature/InputPrizeInfoFeature/Sources/Scene/View/DatePickerField.swift b/Projects/Core/DesignSystem/Sources/DatePicker/DatePickerField.swift similarity index 88% rename from Projects/Feature/InputPrizeInfoFeature/Sources/Scene/View/DatePickerField.swift rename to Projects/Core/DesignSystem/Sources/DatePicker/DatePickerField.swift index bf44884c..a0cd0332 100644 --- a/Projects/Feature/InputPrizeInfoFeature/Sources/Scene/View/DatePickerField.swift +++ b/Projects/Core/DesignSystem/Sources/DatePicker/DatePickerField.swift @@ -1,11 +1,10 @@ -import DesignSystem import SwiftUI -struct DatePickerField: View { +public struct DatePickerField: View { let dateText: String let action: () -> Void - init( + public init( dateText: String, action: @escaping () -> Void ) { @@ -13,7 +12,7 @@ struct DatePickerField: View { self.action = action } - var body: some View { + public var body: some View { SMSTextField( "yyyy.mm", text: Binding( diff --git a/Projects/Core/DesignSystem/Sources/Icon/SMSIcon.swift b/Projects/Core/DesignSystem/Sources/Icon/SMSIcon.swift index 8520184a..579da745 100644 --- a/Projects/Core/DesignSystem/Sources/Icon/SMSIcon.swift +++ b/Projects/Core/DesignSystem/Sources/Icon/SMSIcon.swift @@ -31,12 +31,13 @@ public struct SMSIcon: View { case plus case profile case profileSmallPlus - case redLogout case redPerson case search case smsLogo case trash case leftArrow + case logout + case logoutLine case smallPlus case upArrow case magnifyingglass @@ -103,8 +104,11 @@ public struct SMSIcon: View { case .leftArrow: return DesignSystemAsset.Icons.leftArrow.swiftUIImage - case .redLogout: - return DesignSystemAsset.Icons.redLogout.swiftUIImage + case .logout: + return DesignSystemAsset.Icons.logout.swiftUIImage + + case .logoutLine: + return DesignSystemAsset.Icons.logoutLine.swiftUIImage case .redPerson: return DesignSystemAsset.Icons.redPerson.swiftUIImage diff --git a/Projects/Core/DesignSystem/Sources/SelectionControls/PressedSelectionControlStyle.swift b/Projects/Core/DesignSystem/Sources/SelectionControls/PressedSelectionControlStyle.swift index 3099b41d..4dcb6135 100644 --- a/Projects/Core/DesignSystem/Sources/SelectionControls/PressedSelectionControlStyle.swift +++ b/Projects/Core/DesignSystem/Sources/SelectionControls/PressedSelectionControlStyle.swift @@ -3,10 +3,6 @@ import SwiftUI internal struct PressedSelectionButtonStyle: ButtonStyle { var isSelected: Bool - init(isSelected: Bool) { - self.isSelected = isSelected - } - @ViewBuilder func makeBody(configuration: Configuration) -> some View { Circle() diff --git a/Projects/Domain/AuthDomain/Sources/Repository/AuthRepositoryImpl.swift b/Projects/Domain/AuthDomain/Sources/Repository/AuthRepositoryImpl.swift index 12dae8e0..0a5aaeda 100644 --- a/Projects/Domain/AuthDomain/Sources/Repository/AuthRepositoryImpl.swift +++ b/Projects/Domain/AuthDomain/Sources/Repository/AuthRepositoryImpl.swift @@ -18,8 +18,7 @@ struct AuthRepositoryImpl: AuthRepository { } func logout() async throws { - #warning("통신 오류 남") -// try await remoteAuthDataSource.logout() + try await remoteAuthDataSource.logout() try await localAuthDataSource.logout() } diff --git a/Projects/Domain/AuthDomain/Testing/UseCase/LogoutUseCaseSpy.swift b/Projects/Domain/AuthDomain/Testing/UseCase/LogoutUseCaseSpy.swift new file mode 100644 index 00000000..eb7cef4f --- /dev/null +++ b/Projects/Domain/AuthDomain/Testing/UseCase/LogoutUseCaseSpy.swift @@ -0,0 +1,11 @@ +import AuthDomainInterface + +final class LogoutUseCaseSpy: LogoutUseCase { + var executeCallCount = 0 + var executeHandler: () async throws -> Void = {} + + func execute() async throws { + executeCallCount += 1 + try await executeHandler() + } +} diff --git a/Projects/Domain/AuthDomain/Testing/UseCase/WithdrawalUseCaseSpy.swift b/Projects/Domain/AuthDomain/Testing/UseCase/WithdrawalUseCaseSpy.swift new file mode 100644 index 00000000..b9cff4ac --- /dev/null +++ b/Projects/Domain/AuthDomain/Testing/UseCase/WithdrawalUseCaseSpy.swift @@ -0,0 +1,11 @@ +import AuthDomainInterface + +final class WithdrawalUseCaseSpy: WithdrawalUseCase { + var executeCallCount = 0 + var executeHandler: () async throws -> Void = {} + + func execute() async throws { + executeCallCount += 1 + try await executeHandler() + } +} diff --git a/Projects/Domain/BaseDomain/Sources/RefreshEndpoint/RefreshEndpoint.swift b/Projects/Domain/BaseDomain/Sources/RefreshEndpoint/RefreshEndpoint.swift index e301d88d..2c32d978 100644 --- a/Projects/Domain/BaseDomain/Sources/RefreshEndpoint/RefreshEndpoint.swift +++ b/Projects/Domain/BaseDomain/Sources/RefreshEndpoint/RefreshEndpoint.swift @@ -22,7 +22,7 @@ extension RefreshEndpoint { return .none } - var headers: [String : String]? { + var headers: [String: String]? { switch self { case let .refresh(refreshToken): return [ diff --git a/Projects/Domain/FileDomain/Interface/Error/FileDomainError.swift b/Projects/Domain/FileDomain/Interface/Error/FileDomainError.swift index 98a5c7f0..6108e650 100644 --- a/Projects/Domain/FileDomain/Interface/Error/FileDomainError.swift +++ b/Projects/Domain/FileDomain/Interface/Error/FileDomainError.swift @@ -1,7 +1,6 @@ import Foundation public enum FileDomainError: Error { - case notHwpFile case notImageType case internalServerError } @@ -9,9 +8,6 @@ public enum FileDomainError: Error { extension FileDomainError: LocalizedError { public var errorDescription: String? { switch self { - case .notHwpFile: - return "파일 형식이 hwp 혹은 hwpx인 파일이 아닙니다." - case .notImageType: return "이미지 형식이 jpg, jpeg, png, heic인 이미지가 아닙니다." diff --git a/Projects/Domain/StudentDomain/Interface/DI/StudentDomainBuildable.swift b/Projects/Domain/StudentDomain/Interface/DI/StudentDomainBuildable.swift index 9e1f63c8..a157a1b9 100644 --- a/Projects/Domain/StudentDomain/Interface/DI/StudentDomainBuildable.swift +++ b/Projects/Domain/StudentDomain/Interface/DI/StudentDomainBuildable.swift @@ -3,4 +3,5 @@ public protocol StudentDomainBuildable { var studentRepository: any StudentRepository { get } var fetchStudentListUseCase: any FetchStudentListUseCase { get } var fetchStudentDetailUSeCase: any FetchStudentDetailUseCase { get } + var modifyInformationUseCase: any ModifyInformationUseCase { get } } diff --git a/Projects/Domain/StudentDomain/Interface/DTO/Request/StudentRequestDTO.swift b/Projects/Domain/StudentDomain/Interface/DTO/Request/InputStudentInformationRequestDTO.swift similarity index 87% rename from Projects/Domain/StudentDomain/Interface/DTO/Request/StudentRequestDTO.swift rename to Projects/Domain/StudentDomain/Interface/DTO/Request/InputStudentInformationRequestDTO.swift index ecde9a72..92fd7a06 100644 --- a/Projects/Domain/StudentDomain/Interface/DTO/Request/StudentRequestDTO.swift +++ b/Projects/Domain/StudentDomain/Interface/DTO/Request/InputStudentInformationRequestDTO.swift @@ -1,70 +1,70 @@ import Foundation public struct InputStudentInformationRequestDTO: Encodable { - public let certificate: [String] + public let certificates: [String] public let contactEmail: String public let formOfEmployment: FormOfEmployment public let gsmAuthenticationScore: Int public let introduce: String - public let languageCertificate: [LanguageCertificate] + public let languageCertificates: [LanguageCertificate] public let major: String public let militaryService: MilitaryServiceType public let portfolioURL: String public let profileImgURL: String - public let region: [String] + public let regions: [String] public let salary: Int - public let techStack: [String] + public let techStacks: [String] public let projects: [Project] public let prizes: [Prize] public init( - certificate: [String], + certificates: [String], contactEmail: String, formOfEmployment: FormOfEmployment, gsmAuthenticationScore: Int, introduce: String, - languageCertificate: [LanguageCertificate], + languageCertificates: [LanguageCertificate], major: String, militaryService: MilitaryServiceType, portfolioURL: String, profileImgURL: String, - region: [String], + regions: [String], salary: Int, - techStack: [String], + techStacks: [String], projects: [Project] = [], prizes: [Prize] = [] ) { - self.certificate = certificate + self.certificates = certificates self.contactEmail = contactEmail self.formOfEmployment = formOfEmployment self.gsmAuthenticationScore = gsmAuthenticationScore self.introduce = introduce - self.languageCertificate = languageCertificate + self.languageCertificates = languageCertificates self.major = major self.militaryService = militaryService self.portfolioURL = portfolioURL self.profileImgURL = profileImgURL - self.region = region + self.regions = regions self.salary = salary - self.techStack = techStack + self.techStacks = techStacks self.projects = projects self.prizes = prizes } enum CodingKeys: String, CodingKey { - case certificate + case certificates case contactEmail case formOfEmployment case gsmAuthenticationScore case introduce - case languageCertificate + case languageCertificates case major case militaryService case portfolioURL = "portfolioUrl" case profileImgURL = "profileImgUrl" - case region + case regions case salary - case techStack + case techStacks case projects case prizes } diff --git a/Projects/Domain/StudentDomain/Interface/DTO/Request/ModifyStudentInformationRequestDTO.swift b/Projects/Domain/StudentDomain/Interface/DTO/Request/ModifyStudentInformationRequestDTO.swift new file mode 100644 index 00000000..4169ec4c --- /dev/null +++ b/Projects/Domain/StudentDomain/Interface/DTO/Request/ModifyStudentInformationRequestDTO.swift @@ -0,0 +1,163 @@ +import Foundation + +public struct ModifyStudentInformationRequestDTO: Encodable { + public let certificates: [String] + public let contactEmail: String + public let formOfEmployment: FormOfEmployment + public let gsmAuthenticationScore: Int + public let introduce: String + public let languageCertificates: [LanguageCertificate] + public let major: String + public let militaryService: MilitaryServiceType + public let portfolioURL: String + public let profileImgURL: String + public let regions: [String] + public let salary: Int + public let techStacks: [String] + public let projects: [Project] + public let prizes: [Prize] + + public init( + certificates: [String], + contactEmail: String, + formOfEmployment: FormOfEmployment, + gsmAuthenticationScore: Int, + introduce: String, + languageCertificates: [LanguageCertificate], + major: String, + militaryService: MilitaryServiceType, + portfolioURL: String, + profileImgURL: String, + regions: [String], + salary: Int, + techStacks: [String], + projects: [Project] = [], + prizes: [Prize] = [] + ) { + self.certificates = certificates + self.contactEmail = contactEmail + self.formOfEmployment = formOfEmployment + self.gsmAuthenticationScore = gsmAuthenticationScore + self.introduce = introduce + self.languageCertificates = languageCertificates + self.major = major + self.militaryService = militaryService + self.portfolioURL = portfolioURL + self.profileImgURL = profileImgURL + self.regions = regions + self.salary = salary + self.techStacks = techStacks + self.projects = projects + self.prizes = prizes + } + + enum CodingKeys: String, CodingKey { + case certificates + case contactEmail + case formOfEmployment + case gsmAuthenticationScore + case introduce + case languageCertificates + case major + case militaryService + case portfolioURL = "portfolioUrl" + case profileImgURL = "profileImgUrl" + case regions + case salary + case techStacks + case projects + case prizes + } +} + +public extension ModifyStudentInformationRequestDTO { + struct LanguageCertificate: Encodable { + public let languageCertificateName: String + public let score: String + + public init(languageCertificateName: String, score: String) { + self.languageCertificateName = languageCertificateName + self.score = score + } + } + + struct Project: Encodable { + public let name: String + public let iconImageURL: String + public let previewImageURLs: [String] + public let description: String + public let links: [Link] + public let techStacks: [String] + public let myActivity: String + public let inProgress: InProgress + + public init( + name: String, + iconImageURL: String, + previewImageURLs: [String], + description: String, + links: [Link], + techStacks: [String], + myActivity: String, + inProgress: InProgress + ) { + self.name = name + self.iconImageURL = iconImageURL + self.previewImageURLs = previewImageURLs + self.description = description + self.links = links + self.techStacks = techStacks + self.myActivity = myActivity + self.inProgress = inProgress + } + + enum CodingKeys: String, CodingKey { + case name + case iconImageURL = "icon" + case previewImageURLs = "previewImages" + case description + case links + case techStacks + case myActivity + case inProgress + } + } + + struct Prize: Encodable { + public let name: String + public let type: String + public let date: String + + public init( + name: String, + type: String, + date: String + ) { + self.name = name + self.type = type + self.date = date + } + } +} + +public extension ModifyStudentInformationRequestDTO.Project { + struct Link: Encodable { + public let name: String + public let url: String + + public init(name: String, url: String) { + self.name = name + self.url = url + } + } + + struct InProgress: Encodable { + public let start: String + public let end: String? + + public init(start: String, end: String?) { + self.start = start + self.end = end + } + } +} diff --git a/Projects/Domain/StudentDomain/Interface/DataSource/RemoteStudentDataSource.swift b/Projects/Domain/StudentDomain/Interface/DataSource/RemoteStudentDataSource.swift index 7318889d..2df165b3 100644 --- a/Projects/Domain/StudentDomain/Interface/DataSource/RemoteStudentDataSource.swift +++ b/Projects/Domain/StudentDomain/Interface/DataSource/RemoteStudentDataSource.swift @@ -6,4 +6,5 @@ public protocol RemoteStudentDataSource { func fetchStudentDetailByStudent(userID: String) async throws -> StudentDetailEntity func fetchStudentDetailByGuest(userID: String) async throws -> StudentDetailEntity func fetchStudentDetailByTeacher(userID: String) async throws -> StudentDetailEntity + func modifyInformation(req: ModifyStudentInformationRequestDTO) async throws } diff --git a/Projects/Domain/StudentDomain/Interface/Entity/SingleStudentEntity.swift b/Projects/Domain/StudentDomain/Interface/Entity/SingleStudentEntity.swift index fcbf58da..cc96bd69 100644 --- a/Projects/Domain/StudentDomain/Interface/Entity/SingleStudentEntity.swift +++ b/Projects/Domain/StudentDomain/Interface/Entity/SingleStudentEntity.swift @@ -5,13 +5,13 @@ public struct SingleStudentEntity: Equatable { public let profileImageURL: String public let name: String public let major: String - public let techStack: [String] + public let techStacks: [String] - public init(id: String, profileImageURL: String, name: String, major: String, techStack: [String]) { + public init(id: String, profileImageURL: String, name: String, major: String, techStacks: [String]) { self.id = id self.profileImageURL = profileImageURL self.name = name self.major = major - self.techStack = techStack + self.techStacks = techStacks } } diff --git a/Projects/Domain/StudentDomain/Interface/Enums/SortType.swift b/Projects/Domain/StudentDomain/Interface/Enums/SortType.swift index 8b04064d..da552cf2 100644 --- a/Projects/Domain/StudentDomain/Interface/Enums/SortType.swift +++ b/Projects/Domain/StudentDomain/Interface/Enums/SortType.swift @@ -16,4 +16,3 @@ public extension SortType { } } } - diff --git a/Projects/Domain/StudentDomain/Interface/Repository/StudentRepository.swift b/Projects/Domain/StudentDomain/Interface/Repository/StudentRepository.swift index d39cfd2c..f00dfcbc 100644 --- a/Projects/Domain/StudentDomain/Interface/Repository/StudentRepository.swift +++ b/Projects/Domain/StudentDomain/Interface/Repository/StudentRepository.swift @@ -6,4 +6,5 @@ public protocol StudentRepository { func fetchStudentDetailByStudent(userID: String) async throws -> StudentDetailEntity func fetchStudentDetailByGuest(userID: String) async throws -> StudentDetailEntity func fetchStudentDetailByTeacher(userID: String) async throws -> StudentDetailEntity + func modifyInformation(req: ModifyStudentInformationRequestDTO) async throws } diff --git a/Projects/Domain/StudentDomain/Interface/UseCase/ModifyInformationUseCase.swift b/Projects/Domain/StudentDomain/Interface/UseCase/ModifyInformationUseCase.swift new file mode 100644 index 00000000..6f3ddec6 --- /dev/null +++ b/Projects/Domain/StudentDomain/Interface/UseCase/ModifyInformationUseCase.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol ModifyInformationUseCase { + func execute(req: ModifyStudentInformationRequestDTO) async throws +} diff --git a/Projects/Domain/StudentDomain/Sources/DI/StudentDomainComponent.swift b/Projects/Domain/StudentDomain/Sources/DI/StudentDomainComponent.swift index 082d62f4..dfac8f40 100644 --- a/Projects/Domain/StudentDomain/Sources/DI/StudentDomainComponent.swift +++ b/Projects/Domain/StudentDomain/Sources/DI/StudentDomainComponent.swift @@ -16,6 +16,9 @@ public final class StudentDomainComponent: Component, S public var fetchStudentDetailUSeCase: any FetchStudentDetailUseCase { FetchStudentDetailUseCaseImpl(studentRepository: studentRepository) } + public var modifyInformationUseCase: any ModifyInformationUseCase { + ModifyInformationUseCaseImpl(studentRepository: studentRepository) + } public var studentRepository: any StudentRepository { StudentRepositoryImpl(remoteStudentDataSource: remoteStudentDataSource) } diff --git a/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentDetailByGuestResponseDTO.swift b/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentDetailByGuestResponseDTO.swift index d107646d..a920163c 100644 --- a/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentDetailByGuestResponseDTO.swift +++ b/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentDetailByGuestResponseDTO.swift @@ -6,7 +6,7 @@ public struct FetchStudentDetailByGuestResponseDTO: Decodable { public let introduce: String public let major: String public let profileImg: String - public let techStack: [String] + public let techStacks: [String] public let projects: [ProjectResponseDTO] public let prizes: [PrizeResponseDTO] @@ -15,7 +15,7 @@ public struct FetchStudentDetailByGuestResponseDTO: Decodable { introduce: String, major: String, profileImg: String, - techStack: [String], + techStacks: [String], projects: [ProjectResponseDTO], prizes: [PrizeResponseDTO] ) { @@ -23,7 +23,7 @@ public struct FetchStudentDetailByGuestResponseDTO: Decodable { self.introduce = introduce self.major = major self.profileImg = profileImg - self.techStack = techStack + self.techStacks = techStacks self.projects = projects self.prizes = prizes } @@ -36,7 +36,7 @@ public extension FetchStudentDetailByGuestResponseDTO { introduce: introduce, major: major, profileImageURL: profileImg, - techStacks: techStack, + techStacks: techStacks, projects: projects.map { $0.toDomain() }, prizes: prizes.map { $0.toDomain() } ) diff --git a/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentDetailByStudentResponseDTO.swift b/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentDetailByStudentResponseDTO.swift index 8ee7180f..3b64a26e 100644 --- a/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentDetailByStudentResponseDTO.swift +++ b/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentDetailByStudentResponseDTO.swift @@ -10,7 +10,7 @@ public struct FetchStudentDetailByStudentResponseDTO: Decodable { public let department: DepartmentType public let major: String public let profileImg: String - public let techStack: [String] + public let techStacks: [String] public let projects: [ProjectResponseDTO] public let prizes: [PrizeResponseDTO] } @@ -22,7 +22,7 @@ public extension FetchStudentDetailByStudentResponseDTO { introduce: introduce, major: major, profileImageURL: profileImg, - techStacks: techStack, + techStacks: techStacks, projects: projects.map { $0.toDomain() }, prizes: prizes.map { $0.toDomain() }, detailInfoByStudent: .init( diff --git a/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentListResponseDTO.swift b/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentListResponseDTO.swift index a37c8ee6..de391ba8 100644 --- a/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentListResponseDTO.swift +++ b/Projects/Domain/StudentDomain/Sources/DTO/Response/FetchStudentListResponseDTO.swift @@ -11,7 +11,7 @@ public struct FetchStudentListResponseDTO: Decodable { public let profileImg: String public let name: String public let major: String - public let techStack: [String] + public let techStacks: [String] } enum CodingKeys: String, CodingKey { @@ -27,7 +27,7 @@ public extension FetchStudentListResponseDTO.SingleStudentResponseDTO { profileImageURL: profileImg, name: name, major: major, - techStack: techStack + techStacks: techStacks ) } } diff --git a/Projects/Domain/StudentDomain/Sources/DataSource/RemoteStudentDataSourceImpl.swift b/Projects/Domain/StudentDomain/Sources/DataSource/RemoteStudentDataSourceImpl.swift index 82633c85..032aea2c 100644 --- a/Projects/Domain/StudentDomain/Sources/DataSource/RemoteStudentDataSourceImpl.swift +++ b/Projects/Domain/StudentDomain/Sources/DataSource/RemoteStudentDataSourceImpl.swift @@ -34,4 +34,8 @@ final class RemoteStudentDataSourceImpl: BaseRemoteDataSource, ) .toDomain() } + + func modifyInformation(req: ModifyStudentInformationRequestDTO) async throws { + try await request(.modifyInformation(req)) + } } diff --git a/Projects/Domain/StudentDomain/Sources/Endpoint/StudentEndpoint.swift b/Projects/Domain/StudentDomain/Sources/Endpoint/StudentEndpoint.swift index 44c3f169..b3426b43 100644 --- a/Projects/Domain/StudentDomain/Sources/Endpoint/StudentEndpoint.swift +++ b/Projects/Domain/StudentDomain/Sources/Endpoint/StudentEndpoint.swift @@ -8,6 +8,7 @@ enum StudentEndpoint { case fetchStudentDetailByStudent(userID: String) case fetchStudentDetailByGuest(userID: String) case fetchStudentDetailByTeacher(userID: String) + case modifyInformation(ModifyStudentInformationRequestDTO) } extension StudentEndpoint: SMSEndpoint { @@ -33,6 +34,9 @@ extension StudentEndpoint: SMSEndpoint { case let .fetchStudentDetailByTeacher(userID): return .get("/teacher/\(userID)") + + case .modifyInformation: + return .put("") } } @@ -68,6 +72,9 @@ extension StudentEndpoint: SMSEndpoint { let requestDictionary = Dictionary(uniqueKeysWithValues: requestQuery) return .requestParameters(query: requestDictionary) + case let .modifyInformation(req): + return .requestJSONEncodable(req) + default: return .requestPlain } @@ -105,6 +112,11 @@ extension StudentEndpoint: SMSEndpoint { case .fetchStudentDetailByTeacher: return [:] + + case .modifyInformation: + return [ + 400: .invalidRequest + ] } } } diff --git a/Projects/Domain/StudentDomain/Sources/Repository/StudentRepositoryImpl.swift b/Projects/Domain/StudentDomain/Sources/Repository/StudentRepositoryImpl.swift index d675f06c..60548584 100644 --- a/Projects/Domain/StudentDomain/Sources/Repository/StudentRepositoryImpl.swift +++ b/Projects/Domain/StudentDomain/Sources/Repository/StudentRepositoryImpl.swift @@ -27,4 +27,8 @@ struct StudentRepositoryImpl: StudentRepository { func fetchStudentDetailByTeacher(userID: String) async throws -> StudentDetailEntity { try await remoteStudentDataSource.fetchStudentDetailByTeacher(userID: userID) } + + func modifyInformation(req: ModifyStudentInformationRequestDTO) async throws { + try await remoteStudentDataSource.modifyInformation(req: req) + } } diff --git a/Projects/Domain/StudentDomain/Sources/UseCase/ModifyInformationUseCaseImpl.swift b/Projects/Domain/StudentDomain/Sources/UseCase/ModifyInformationUseCaseImpl.swift new file mode 100644 index 00000000..c87f61e5 --- /dev/null +++ b/Projects/Domain/StudentDomain/Sources/UseCase/ModifyInformationUseCaseImpl.swift @@ -0,0 +1,13 @@ +import StudentDomainInterface + +struct ModifyInformationUseCaseImpl: ModifyInformationUseCase { + private let studentRepository: any StudentRepository + + init(studentRepository: any StudentRepository) { + self.studentRepository = studentRepository + } + + func execute(req: ModifyStudentInformationRequestDTO) async throws { + try await studentRepository.modifyInformation(req: req) + } +} diff --git a/Projects/Domain/StudentDomain/Testing/UseCase/InputInformationUseCaseSpy.swift b/Projects/Domain/StudentDomain/Testing/UseCase/InputInformationUseCaseSpy.swift new file mode 100644 index 00000000..580c220b --- /dev/null +++ b/Projects/Domain/StudentDomain/Testing/UseCase/InputInformationUseCaseSpy.swift @@ -0,0 +1,8 @@ +import StudentDomainInterface + +final class InputInformationUseCaseSpy: InputInformationUseCase { + var executeCallCount = 0 + func execute(req: InputStudentInformationRequestDTO) async throws { + executeCallCount += 1 + } +} diff --git a/Projects/Domain/StudentDomain/Testing/UseCase/ModifyInformationUseCaseSpy.swift b/Projects/Domain/StudentDomain/Testing/UseCase/ModifyInformationUseCaseSpy.swift new file mode 100644 index 00000000..2ec18e31 --- /dev/null +++ b/Projects/Domain/StudentDomain/Testing/UseCase/ModifyInformationUseCaseSpy.swift @@ -0,0 +1,8 @@ +import StudentDomainInterface + +final class ModifyInformationUseCaseSpy: ModifyInformationUseCase { + var executeCallCount = 0 + func execute(req: ModifyStudentInformationRequestDTO) async throws { + executeCallCount += 1 + } +} diff --git a/Projects/Domain/TechStackDomain/Sources/DTO/Response/FetchTechStackListResponseDTO.swift b/Projects/Domain/TechStackDomain/Sources/DTO/Response/FetchTechStackListResponseDTO.swift index b1e5cd69..d3ca1d3c 100644 --- a/Projects/Domain/TechStackDomain/Sources/DTO/Response/FetchTechStackListResponseDTO.swift +++ b/Projects/Domain/TechStackDomain/Sources/DTO/Response/FetchTechStackListResponseDTO.swift @@ -1,9 +1,9 @@ import Foundation public struct FetchTechStackListResponseDTO: Decodable { - public let techStack: [String] + public let techStacks: [String] - public init(techStack: [String]) { - self.techStack = techStack + public init(techStacks: [String]) { + self.techStacks = techStacks } } diff --git a/Projects/Domain/TechStackDomain/Sources/DataSource/RemoteTechStackDataSourceImpl.swift b/Projects/Domain/TechStackDomain/Sources/DataSource/RemoteTechStackDataSourceImpl.swift index 8feaa054..c3e1737f 100644 --- a/Projects/Domain/TechStackDomain/Sources/DataSource/RemoteTechStackDataSourceImpl.swift +++ b/Projects/Domain/TechStackDomain/Sources/DataSource/RemoteTechStackDataSourceImpl.swift @@ -7,6 +7,6 @@ final class RemoteTechStackDataSourceImpl: RemoteTechStackDataSource { func fetchTechStackList(keyword: String) async throws -> [String] { try await request(.fetchTechStackList(keyword: keyword), dto: FetchTechStackListResponseDTO.self) - .techStack + .techStacks } } diff --git a/Projects/Domain/UserDomain/Interface/Entity/MyPageEntity.swift b/Projects/Domain/UserDomain/Interface/Entity/MyPageEntity.swift index f221e3aa..eb782156 100644 --- a/Projects/Domain/UserDomain/Interface/Entity/MyPageEntity.swift +++ b/Projects/Domain/UserDomain/Interface/Entity/MyPageEntity.swift @@ -20,6 +20,8 @@ public struct MyPageEntity: Equatable { public let languageCertificates: [LanguageCertificateEntity] public let certificates: [String] public let techStacks: [String] + public let proejcts: [ProjectEntity] + public let prizes: [PrizeEntity] public init( name: String, @@ -39,7 +41,9 @@ public struct MyPageEntity: Equatable { salary: Int, languageCertificates: [LanguageCertificateEntity], certificates: [String], - techStacks: [String] + techStacks: [String], + projects: [ProjectEntity], + prizes: [PrizeEntity] ) { self.name = name self.introduce = introduce @@ -59,5 +63,7 @@ public struct MyPageEntity: Equatable { self.languageCertificates = languageCertificates self.certificates = certificates self.techStacks = techStacks + self.proejcts = projects + self.prizes = prizes } } diff --git a/Projects/Domain/UserDomain/Interface/Error/UserDomainError.swift b/Projects/Domain/UserDomain/Interface/Error/UserDomainError.swift index 0c6d6735..7df04c85 100644 --- a/Projects/Domain/UserDomain/Interface/Error/UserDomainError.swift +++ b/Projects/Domain/UserDomain/Interface/Error/UserDomainError.swift @@ -1,5 +1,5 @@ import Foundation public enum UserDomainError: Error { - + } diff --git a/Projects/Domain/UserDomain/Sources/DTO/Response/FetchMyProfileResponseDTO.swift b/Projects/Domain/UserDomain/Sources/DTO/Response/FetchMyProfileResponseDTO.swift index 0a4910f1..c323e424 100644 --- a/Projects/Domain/UserDomain/Sources/DTO/Response/FetchMyProfileResponseDTO.swift +++ b/Projects/Domain/UserDomain/Sources/DTO/Response/FetchMyProfileResponseDTO.swift @@ -21,6 +21,31 @@ struct FetchMyProfileResponseDTO: Decodable { let languageCertificates: [LanguageCertificateResponseDTO] let certificates: [String] let techStacks: [String] + let projects: [ProjectResponseDTO] + let prizes: [PrizeResponseDTO] + + enum CodingKeys: String, CodingKey { + case name + case introduce + case portfolioURL = "portfolioUrl" + case grade + case classNum + case number + case department + case major + case profileImageURL = "profileImg" + case contactEmail + case gsmAuthenticationScore + case formOfEmployment + case regions + case militaryService + case salary + case languageCertificates + case certificates + case techStacks + case projects + case prizes + } } extension FetchMyProfileResponseDTO { @@ -33,14 +58,128 @@ extension FetchMyProfileResponseDTO { case score } } + + struct ProjectResponseDTO: Decodable { + public let name: String + public let iconImageURL: String + public let previewImageURLs: [String] + public let description: String + public let links: [Link] + public let techStacks: [String] + public let myActivity: String + public let inProgress: InProgress + + public init( + name: String, + iconImageURL: String, + previewImageURLs: [String], + description: String, + links: [Link], + techStacks: [String], + myActivity: String, + inProgress: InProgress + ) { + self.name = name + self.iconImageURL = iconImageURL + self.previewImageURLs = previewImageURLs + self.description = description + self.links = links + self.techStacks = techStacks + self.myActivity = myActivity + self.inProgress = inProgress + } + + enum CodingKeys: String, CodingKey { + case name + case iconImageURL = "icon" + case previewImageURLs = "previewImages" + case description + case links + case techStacks + case myActivity + case inProgress + } + } + + struct PrizeResponseDTO: Decodable { + public let name: String + public let type: String + public let date: String + + public init( + name: String, + type: String, + date: String + ) { + self.name = name + self.type = type + self.date = date + } + } } +extension FetchMyProfileResponseDTO.ProjectResponseDTO { + struct Link: Decodable { + public let name: String + public let url: String + + public init(name: String, url: String) { + self.name = name + self.url = url + } + } + + struct InProgress: Decodable { + public let start: String + public let end: String? + + public init(start: String, end: String?) { + self.start = start + self.end = end + } + } +} + + extension FetchMyProfileResponseDTO.LanguageCertificateResponseDTO { func toDomain() -> LanguageCertificateEntity { LanguageCertificateEntity(name: name, score: score) } } +extension FetchMyProfileResponseDTO.ProjectResponseDTO { + func toDomain() -> ProjectEntity { + ProjectEntity( + name: name, + iconImageURL: iconImageURL, + previewImageURLs: previewImageURLs, + description: description, + links: links.map { $0.toDomain() }, + techStacks: techStacks, + myActivity: myActivity, + inProgress: inProgress.toDomain() + ) + } +} + +extension FetchMyProfileResponseDTO.PrizeResponseDTO { + func toDomain() -> PrizeEntity { + PrizeEntity(name: name, type: type, date: date) + } +} + +extension FetchMyProfileResponseDTO.ProjectResponseDTO.Link { + func toDomain() -> ProjectEntity.LinkEntity { + ProjectEntity.LinkEntity(name: name, url: url) + } +} + +extension FetchMyProfileResponseDTO.ProjectResponseDTO.InProgress { + func toDomain() -> ProjectEntity.InProgressEntity { + ProjectEntity.InProgressEntity(start: start, end: end) + } +} + extension FetchMyProfileResponseDTO { func toDomain() -> MyPageEntity { MyPageEntity( @@ -61,7 +200,9 @@ extension FetchMyProfileResponseDTO { salary: salary, languageCertificates: languageCertificates.map { $0.toDomain() }, certificates: certificates, - techStacks: techStacks + techStacks: techStacks, + projects: projects.map { $0.toDomain() }, + prizes: prizes.map { $0.toDomain() } ) } } diff --git a/Projects/Domain/UserDomain/Testing/UseCase/FetchMyProfileUseCaseSpy.swift b/Projects/Domain/UserDomain/Testing/UseCase/FetchMyProfileUseCaseSpy.swift new file mode 100644 index 00000000..26374f94 --- /dev/null +++ b/Projects/Domain/UserDomain/Testing/UseCase/FetchMyProfileUseCaseSpy.swift @@ -0,0 +1,65 @@ +import UserDomainInterface + +final class FetchMyProfileUseCaseSpy: FetchMyProfileUseCase { + var executeCallCount = 0 + var executeHandler: () async throws -> MyPageEntity = { + .init( + name: "name", + introduce: "intro", + portfolioURL: "https://github.com/baekteun", + grade: 3, + classNum: 2, + number: 18, + department: .software, + major: "iOS", + profileImageURL: "https://avatars.githubusercontent.com/u/74440939?v=4", + contactEmail: "baegteun@gmail.com", + gsmAuthenticationScore: 990, + formOfEmployment: .fullTime, + regions: ["Seoul"], + militaryService: .hope, + salary: 9999, + languageCertificates: [ + .init(name: "한국어", score: "원어민") + ], + certificates: [ + "정보처리산업기사" + ], + techStacks: [ + "Swift", + "Tuist", + "MicroFeatures" + ], + projects: [ + .init( + name: "asdf", + iconImageURL: "https://avatars.githubusercontent.com/u/74440939?v=4", + previewImageURLs: [ + "https://avatars.githubusercontent.com/u/74440939?v=4" + ], + description: "최형우다", + links: [ + .init( + name: "와우", + url: "https://www.github.com" + ) + ], + techStacks: [ + "iOS" + ], + myActivity: "나는 짱이다", + inProgress: .init(start: "", end: "") + ) + ], + prizes: [ + .init(name: "1234", type: "와우", date: "2023.12.11") + ] + ) + } + + func execute() async throws -> MyPageEntity { + executeCallCount += 1 + return try await executeHandler() + } + +} diff --git a/Projects/Feature/InputInformationFeature/Sources/Intent/InputInformationIntent.swift b/Projects/Feature/InputInformationFeature/Sources/Intent/InputInformationIntent.swift index 3fc5b777..a8bc6972 100644 --- a/Projects/Feature/InputInformationFeature/Sources/Intent/InputInformationIntent.swift +++ b/Projects/Feature/InputInformationFeature/Sources/Intent/InputInformationIntent.swift @@ -34,7 +34,6 @@ final class InputInformationIntent: InputInformationIntentProtocol { func completeToInputAllInfo(state: any InputInformationStateProtocol) { guard let inputProfileInfo = state.inputProfileInformationObject, - let inputSchoolLifeInfo = state.inputSchoolLifeInformationObject, let inputWorkInfo = state.inputWorkInfomationObject, let militaryServiceType = state.militaryServiceType else { @@ -51,19 +50,19 @@ final class InputInformationIntent: InputInformationIntentProtocol { ) let inputInformationRequest = try await InputStudentInformationRequestDTO( - certificate: state.certificates, + certificates: state.certificates, contactEmail: inputProfileInfo.contactEmail, formOfEmployment: FormOfEmployment(rawValue: inputWorkInfo.formOfEmployment) ?? .fullTime, - gsmAuthenticationScore: inputSchoolLifeInfo.gsmAuthenticationScore, + gsmAuthenticationScore: state.gsmAuthenticationScore, introduce: inputProfileInfo.introduce, - languageCertificate: state.languages, + languageCertificates: state.languages, major: inputProfileInfo.major, militaryService: militaryServiceType, portfolioURL: inputProfileInfo.portfoiloURL, profileImgURL: profileImageURL, - region: inputWorkInfo.workRegion, + regions: inputWorkInfo.workRegion, salary: inputWorkInfo.salary, - techStack: inputProfileInfo.techStack, + techStacks: inputProfileInfo.techStacks, projects: state.projects.concurrentMap { async let imageURL = self.imageUploadUseCase.execute( image: $0.iconImage?.data ?? .init(), @@ -117,8 +116,8 @@ extension InputInformationIntent: InputSchoolLifeDelegate { model?.prevButtonDidTap() } - func completeToInputSchoolLife(input: InputSchoolLifeInformationObject) { - model?.updateInputSchoolLifeInformationObject(object: input) + func completeToInputSchoolLife(gsmAuthenticationScore: Int) { + model?.updateInputSchoolLifeInformationObject(gsmAuthenticationScore: gsmAuthenticationScore) model?.nextButtonDidTap() } } diff --git a/Projects/Feature/InputInformationFeature/Sources/Model/InputInformationModel.swift b/Projects/Feature/InputInformationFeature/Sources/Model/InputInformationModel.swift index 9cf8430a..9b60b8dc 100644 --- a/Projects/Feature/InputInformationFeature/Sources/Model/InputInformationModel.swift +++ b/Projects/Feature/InputInformationFeature/Sources/Model/InputInformationModel.swift @@ -12,7 +12,7 @@ final class InputInformationModel: ObservableObject, InputInformationStateProtoc @Published var isError: Bool = false var errorMessage: String = "알 수 없는 오류가 발생했습니다." var inputProfileInformationObject: InputProfileInformationObject? - var inputSchoolLifeInformationObject: InputSchoolLifeInformationObject? + var gsmAuthenticationScore: Int = 0 var inputWorkInfomationObject: InputWorkInformationObject? var certificates: [String] = [] var militaryServiceType: MilitaryServiceType? @@ -41,8 +41,8 @@ extension InputInformationModel: InputInformationActionProtocol { self.inputProfileInformationObject = object } - func updateInputSchoolLifeInformationObject(object: InputSchoolLifeInformationObject) { - self.inputSchoolLifeInformationObject = object + func updateInputSchoolLifeInformationObject(gsmAuthenticationScore: Int) { + self.gsmAuthenticationScore = gsmAuthenticationScore } func updateInputWorkInformationObject(object: InputWorkInformationObject) { diff --git a/Projects/Feature/InputInformationFeature/Sources/Model/InputInformationModelProtocol.swift b/Projects/Feature/InputInformationFeature/Sources/Model/InputInformationModelProtocol.swift index a69540e7..1df39155 100644 --- a/Projects/Feature/InputInformationFeature/Sources/Model/InputInformationModelProtocol.swift +++ b/Projects/Feature/InputInformationFeature/Sources/Model/InputInformationModelProtocol.swift @@ -23,7 +23,7 @@ protocol InputInformationStateProtocol { var isError: Bool { get } var errorMessage: String { get } var inputProfileInformationObject: InputProfileInformationObject? { get } - var inputSchoolLifeInformationObject: InputSchoolLifeInformationObject? { get } + var gsmAuthenticationScore: Int { get } var inputWorkInfomationObject: InputWorkInformationObject? { get } var certificates: [String] { get } var militaryServiceType: MilitaryServiceType? { get } @@ -37,7 +37,7 @@ protocol InputInformationActionProtocol: AnyObject { func prevButtonDidTap() func nextButtonDidTap() func updateInputProfileInformationObject(object: InputProfileInformationObject) - func updateInputSchoolLifeInformationObject(object: InputSchoolLifeInformationObject) + func updateInputSchoolLifeInformationObject(gsmAuthenticationScore: Int) func updateInputWorkInformationObject(object: InputWorkInformationObject) func updateCertificates(certificates: [String]) func updateMilitaryServiceType(type: MilitaryServiceType) diff --git a/Projects/Feature/InputLanguageInfoFeature/Sources/Model/LanguageInputModel.swift b/Projects/Feature/InputLanguageInfoFeature/Sources/Model/LanguageInputModel.swift index c6eefa74..66176d52 100644 --- a/Projects/Feature/InputLanguageInfoFeature/Sources/Model/LanguageInputModel.swift +++ b/Projects/Feature/InputLanguageInfoFeature/Sources/Model/LanguageInputModel.swift @@ -3,9 +3,4 @@ import Foundation struct LanguageInputModel: Equatable { let languageName: String let languageScore: String - - init(languageName: String, languageScore: String) { - self.languageName = languageName - self.languageScore = languageScore - } } diff --git a/Projects/Feature/InputPrizeInfoFeature/Sources/Model/InputPrizeInfoModel.swift b/Projects/Feature/InputPrizeInfoFeature/Sources/Model/InputPrizeInfoModel.swift index 8773e0be..8151ff1a 100644 --- a/Projects/Feature/InputPrizeInfoFeature/Sources/Model/InputPrizeInfoModel.swift +++ b/Projects/Feature/InputPrizeInfoFeature/Sources/Model/InputPrizeInfoModel.swift @@ -52,5 +52,4 @@ extension InputPrizeInfoModel: InputPrizeInfoActionProtocol { func updateIsPresentedPrizeAtDatePicker(isPresented: Bool) { self.isPresentedPrizeAtDatePicker = isPresented } - } diff --git a/Projects/Feature/InputPrizeInfoFeature/Sources/Scene/InputPrizeInfoView.swift b/Projects/Feature/InputPrizeInfoFeature/Sources/Scene/InputPrizeInfoView.swift index 2f300f4f..f9143c75 100644 --- a/Projects/Feature/InputPrizeInfoFeature/Sources/Scene/InputPrizeInfoView.swift +++ b/Projects/Feature/InputPrizeInfoFeature/Sources/Scene/InputPrizeInfoView.swift @@ -67,9 +67,9 @@ struct InputPrizeInfoView: View { let collapsed = state.collapsedPrize[safe: index] ?? false VStack(alignment: .leading, spacing: 24) { HStack(spacing: 16) { - SMSText("수상", font: .title1) + let prizeName = state.prizeList[safe: index]?.name ?? "" + SMSText(prizeName.isEmpty ? "수상" : prizeName, font: .title1) .foregroundColor(.sms(.system(.black))) - Spacer() SMSIcon(.downChevron) diff --git a/Projects/Feature/InputProfileInfoFeature/Interface/InputProfileDelegate.swift b/Projects/Feature/InputProfileInfoFeature/Interface/InputProfileDelegate.swift index d00285f0..11187218 100644 --- a/Projects/Feature/InputProfileInfoFeature/Interface/InputProfileDelegate.swift +++ b/Projects/Feature/InputProfileInfoFeature/Interface/InputProfileDelegate.swift @@ -11,7 +11,7 @@ public struct InputProfileInformationObject { public let contactEmail: String public let major: String public let portfoiloURL: String - public let techStack: [String] + public let techStacks: [String] public init( profileImageData: Data, @@ -20,7 +20,7 @@ public struct InputProfileInformationObject { contactEmail: String, major: String, portfoiloURL: String, - techStack: [String] + techStacks: [String] ) { self.profileImageData = profileImageData self.profileImageFilename = profileImageFilename @@ -28,6 +28,6 @@ public struct InputProfileInformationObject { self.contactEmail = contactEmail self.major = major self.portfoiloURL = portfoiloURL - self.techStack = techStack + self.techStacks = techStacks } } diff --git a/Projects/Feature/InputProfileInfoFeature/Sources/Intent/InputProfileInfoIntent.swift b/Projects/Feature/InputProfileInfoFeature/Sources/Intent/InputProfileInfoIntent.swift index 5b4ae559..4b14872e 100644 --- a/Projects/Feature/InputProfileInfoFeature/Sources/Intent/InputProfileInfoIntent.swift +++ b/Projects/Feature/InputProfileInfoFeature/Sources/Intent/InputProfileInfoIntent.swift @@ -120,7 +120,7 @@ final class InputProfileInfoIntent: InputProfileInfoIntentProtocol { contactEmail: state.email, major: state.major, portfoiloURL: state.portfolioURL, - techStack: state.techStacks + techStacks: state.techStacks ) inputProfileDelegate?.completeToInputProfile(input: input) } diff --git a/Projects/Feature/InputProfileInfoFeature/Sources/Scene/View/ImageMethodPickerView.swift b/Projects/Feature/InputProfileInfoFeature/Sources/Scene/View/ImageMethodPickerView.swift index 395e5477..729e673d 100644 --- a/Projects/Feature/InputProfileInfoFeature/Sources/Scene/View/ImageMethodPickerView.swift +++ b/Projects/Feature/InputProfileInfoFeature/Sources/Scene/View/ImageMethodPickerView.swift @@ -16,11 +16,11 @@ struct ImageMethodPickerView: View { var body: some View { VStack(spacing: 28) { Group { - ImageMethodRowView(title: "앨범에서 가져오기", icon: .photo) { + ImageMethodRowView(title: "앨범", icon: .photo) { albumAction() } - ImageMethodRowView(title: "카메라에서 촬영하기", icon: .camera) { + ImageMethodRowView(title: "카메라", icon: .camera) { cameraAction() } } diff --git a/Projects/Feature/InputProfileInfoFeature/Sources/Scene/View/ImageMethodRowView.swift b/Projects/Feature/InputProfileInfoFeature/Sources/Scene/View/ImageMethodRowView.swift index 12846ab9..1384e9d4 100644 --- a/Projects/Feature/InputProfileInfoFeature/Sources/Scene/View/ImageMethodRowView.swift +++ b/Projects/Feature/InputProfileInfoFeature/Sources/Scene/View/ImageMethodRowView.swift @@ -21,7 +21,7 @@ struct ImageMethodRowView: View { action() } label: { Label { - SMSText(title, font: .body1) + SMSText(title, font: .title2) } icon: { SMSIcon(icon) } diff --git a/Projects/Feature/InputProjectInfoFeature/Sources/Intent/InputProjectInfoIntent.swift b/Projects/Feature/InputProjectInfoFeature/Sources/Intent/InputProjectInfoIntent.swift index 887d8582..c552a602 100644 --- a/Projects/Feature/InputProjectInfoFeature/Sources/Intent/InputProjectInfoIntent.swift +++ b/Projects/Feature/InputProjectInfoFeature/Sources/Intent/InputProjectInfoIntent.swift @@ -71,9 +71,13 @@ final class InputProjectInfoIntent: InputProjectInfoIntentProtocol { model?.updateIconImage(index: index, image: image) } - func appendPreviewImageButtonDidTap(index: Int) { - model?.updateFocusedProjectIndex(index: index) - model?.updateIsPresentedPreviewImagePicker(isPresented: true) + func appendPreviewImageButtonDidTap(index: Int, previewsCount: Int) { + if previewsCount == 4 { + model?.updateIsPresentedToast(isPresented: true) + } else { + model?.updateFocusedProjectIndex(index: index) + model?.updateIsPresentedPreviewImagePicker(isPresented: true) + } } func appendPreviewImage(index: Int, image: PickedImageResult) { @@ -100,16 +104,6 @@ final class InputProjectInfoIntent: InputProjectInfoIntentProtocol { model?.updateProjectMainTask(index: index, mainTask: mainTask) } - func projectStartAtButtonDidTap(index: Int) { - model?.updateFocusedProjectIndex(index: index) - model?.updateIsPresentedStartAtDatePicker(isPresented: true) - } - - func projectEndAtButtonDidTap(index: Int) { - model?.updateFocusedProjectIndex(index: index) - model?.updateIsPresentedEndAtDatePicker(isPresented: true) - } - func projectIsInProgressButtonDidTap(index: Int, isInProgress: Bool) { model?.updateIsInProgress(index: index, isInProgress: isInProgress) } @@ -185,4 +179,8 @@ final class InputProjectInfoIntent: InputProjectInfoIntentProtocol { func techStackAppendDismissed() { model?.updateIsPresentedTechStackAppend(isPresented: false) } + + func toastDismissed() { + model?.updateIsPresentedToast(isPresented: false) + } } diff --git a/Projects/Feature/InputProjectInfoFeature/Sources/Intent/InputProjectInfoIntentProtocol.swift b/Projects/Feature/InputProjectInfoFeature/Sources/Intent/InputProjectInfoIntentProtocol.swift index a3131b88..8e089af8 100644 --- a/Projects/Feature/InputProjectInfoFeature/Sources/Intent/InputProjectInfoIntentProtocol.swift +++ b/Projects/Feature/InputProjectInfoFeature/Sources/Intent/InputProjectInfoIntentProtocol.swift @@ -7,15 +7,13 @@ protocol InputProjectInfoIntentProtocol { func projectToggleButtonDidTap(index: Int) func updateProjectName(index: Int, name: String) func updateIconImage(index: Int, image: PickedImageResult) - func appendPreviewImageButtonDidTap(index: Int) + func appendPreviewImageButtonDidTap(index: Int, previewsCount: Int) func appendPreviewImage(index: Int, image: PickedImageResult) func removePreviewImageDidTap(index: Int, previewIndex: Int) func updateProjectContent(index: Int, content: String) func techStacksDidSelect(index: Int, techStacks: [String]) func removeProjectTechStackButtonDidTap(index: Int, techStack: String) func updateProjectMainTask(index: Int, mainTask: String) - func projectStartAtButtonDidTap(index: Int) - func projectEndAtButtonDidTap(index: Int) func projectStartAtDidSelect(index: Int, startAt: Date) func projectEndAtDidSelect(index: Int, endAt: Date) func projectIsInProgressButtonDidTap(index: Int, isInProgress: Bool) @@ -34,4 +32,5 @@ protocol InputProjectInfoIntentProtocol { func endAtDatePickerDismissed() func techStackAppendButtonDidTap(index: Int) func techStackAppendDismissed() + func toastDismissed() } diff --git a/Projects/Feature/InputProjectInfoFeature/Sources/Model/InputProjectInfoModel.swift b/Projects/Feature/InputProjectInfoFeature/Sources/Model/InputProjectInfoModel.swift index bf715843..6a635fa5 100644 --- a/Projects/Feature/InputProjectInfoFeature/Sources/Model/InputProjectInfoModel.swift +++ b/Projects/Feature/InputProjectInfoFeature/Sources/Model/InputProjectInfoModel.swift @@ -11,6 +11,7 @@ final class InputProjectInfoModel: ObservableObject, InputProjectInfoStateProtoc @Published var isPresentedStartAtDatePicker: Bool = false @Published var isPresentedEndAtDatePicker: Bool = false @Published var isPresentedTechStackAppend: Bool = false + @Published var isPresentedToast: Bool = false var focusedProjectIndex: Int = 0 } @@ -152,4 +153,8 @@ extension InputProjectInfoModel: InputProjectInfoActionProtocol { func updateIsPresentedTechStackAppend(isPresented: Bool) { self.isPresentedTechStackAppend = isPresented } + + func updateIsPresentedToast(isPresented: Bool) { + self.isPresentedToast = isPresented + } } diff --git a/Projects/Feature/InputProjectInfoFeature/Sources/Model/InputProjectInfoModelProtocol.swift b/Projects/Feature/InputProjectInfoFeature/Sources/Model/InputProjectInfoModelProtocol.swift index d87208f5..dbf20e33 100644 --- a/Projects/Feature/InputProjectInfoFeature/Sources/Model/InputProjectInfoModelProtocol.swift +++ b/Projects/Feature/InputProjectInfoFeature/Sources/Model/InputProjectInfoModelProtocol.swift @@ -49,6 +49,7 @@ protocol InputProjectInfoStateProtocol { var isPresentedStartAtDatePicker: Bool { get } var isPresentedEndAtDatePicker: Bool { get } var isPresentedTechStackAppend: Bool { get } + var isPresentedToast: Bool { get } } protocol InputProjectInfoActionProtocol: AnyObject { @@ -76,4 +77,5 @@ protocol InputProjectInfoActionProtocol: AnyObject { func updateIsPresentedStartAtDatePicker(isPresented: Bool) func updateIsPresentedEndAtDatePicker(isPresented: Bool) func updateIsPresentedTechStackAppend(isPresented: Bool) + func updateIsPresentedToast(isPresented: Bool) } diff --git a/Projects/Feature/InputProjectInfoFeature/Sources/Scene/InputProjectInfoView.swift b/Projects/Feature/InputProjectInfoFeature/Sources/Scene/InputProjectInfoView.swift index 343058c3..71d48727 100644 --- a/Projects/Feature/InputProjectInfoFeature/Sources/Scene/InputProjectInfoView.swift +++ b/Projects/Feature/InputProjectInfoFeature/Sources/Scene/InputProjectInfoView.swift @@ -75,6 +75,13 @@ struct InputProjectInfoView: View { } ) ) + .smsToast( + text: "이미지는 최대 4개까지만 추가 할 수 있어요.", + isShowing: Binding( + get: { state.isPresentedToast }, + set: { _ in intent.toastDismissed() } + ) + ) .datePicker( isShowing: Binding( get: { state.isPresentedStartAtDatePicker }, @@ -113,7 +120,9 @@ struct InputProjectInfoView: View { let collapsed = state.collapsedProject[safe: index] ?? false VStack(alignment: .leading, spacing: 24) { HStack(spacing: 16) { - SMSText("프로젝트", font: .title1) + let projectName = state.projectList[safe: index]?.name ?? "" + + SMSText(projectName.isEmpty ? "프로젝트" : projectName, font: .title1) .foregroundColor(.sms(.system(.black))) Spacer() @@ -200,9 +209,9 @@ private extension InputProjectInfoView { @ViewBuilder func projectPreviewImageList(index: Int) -> some View { - LazyHStack(spacing: 8) { - let projectPreviewImages = state.projectList[safe: index]?.previewImages ?? [] - ConditionView(projectPreviewImages.count < 4) { + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 8) { + let projectPreviewImages = state.projectList[safe: index]?.previewImages ?? [] imagePlaceholder(size: 132) .overlay { VStack(spacing: 4) { @@ -216,22 +225,22 @@ private extension InputProjectInfoView { } } .buttonWrapper { - intent.appendPreviewImageButtonDidTap(index: index) + intent.appendPreviewImageButtonDidTap(index: index, previewsCount: projectPreviewImages.count) } - } - ForEach(projectPreviewImages.indices, id: \.self) { previewIndex in - Image(uiImage: projectPreviewImages[previewIndex].uiImage) - .resizable() - .frame(width: 132, height: 132) - .cornerRadius(8) - .overlay(alignment: .topTrailing) { - SMSIcon(.xmark) - .padding(4) - .buttonWrapper { - intent.removePreviewImageDidTap(index: index, previewIndex: previewIndex) - } - } + ForEach(projectPreviewImages.indices, id: \.self) { previewIndex in + Image(uiImage: projectPreviewImages[previewIndex].uiImage) + .resizable() + .frame(width: 132, height: 132) + .cornerRadius(8) + .overlay(alignment: .topTrailing) { + SMSIcon(.xmark) + .padding(4) + .buttonWrapper { + intent.removePreviewImageDidTap(index: index, previewIndex: previewIndex) + } + } + } } } .titleWrapper("미리보기 사진") diff --git a/Projects/Feature/InputProjectInfoFeature/Sources/Scene/View/DatePickerField.swift b/Projects/Feature/InputProjectInfoFeature/Sources/Scene/View/DatePickerField.swift deleted file mode 100644 index bf44884c..00000000 --- a/Projects/Feature/InputProjectInfoFeature/Sources/Scene/View/DatePickerField.swift +++ /dev/null @@ -1,37 +0,0 @@ -import DesignSystem -import SwiftUI - -struct DatePickerField: View { - let dateText: String - let action: () -> Void - - init( - dateText: String, - action: @escaping () -> Void - ) { - self.dateText = dateText - self.action = action - } - - var body: some View { - SMSTextField( - "yyyy.mm", - text: Binding( - get: { dateText }, - set: { _ in } - ), - isOnClear: false - ) - .disabled(true) - .overlay(alignment: .trailing) { - SMSIcon(.calendar) - .padding(.trailing, 12) - } - .simultaneousGesture( - TapGesture() - .onEnded { - action() - } - ) - } -} diff --git a/Projects/Feature/InputSchoolLifeInfoFeature/Interface/InputSchoolLifeDelegate.swift b/Projects/Feature/InputSchoolLifeInfoFeature/Interface/InputSchoolLifeDelegate.swift index abc7582c..feaa82e7 100644 --- a/Projects/Feature/InputSchoolLifeInfoFeature/Interface/InputSchoolLifeDelegate.swift +++ b/Projects/Feature/InputSchoolLifeInfoFeature/Interface/InputSchoolLifeDelegate.swift @@ -2,17 +2,5 @@ import Foundation public protocol InputSchoolLifeDelegate: AnyObject { func schoolLifePrevButtonDidTap() - func completeToInputSchoolLife(input: InputSchoolLifeInformationObject) -} - -public struct InputSchoolLifeInformationObject { - public let hwpFilename: String - public let gsmAuthenticationScore: Int - public let hwpData: Data - - public init(hwpFilename: String, gsmAuthenticationScore: Int, hwpData: Data) { - self.hwpFilename = hwpFilename - self.gsmAuthenticationScore = gsmAuthenticationScore - self.hwpData = hwpData - } + func completeToInputSchoolLife(gsmAuthenticationScore: Int) } diff --git a/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Intent/InputSchoolLifeInfoIntent.swift b/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Intent/InputSchoolLifeInfoIntent.swift index c17e8da8..d4d02b5d 100644 --- a/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Intent/InputSchoolLifeInfoIntent.swift +++ b/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Intent/InputSchoolLifeInfoIntent.swift @@ -18,22 +18,6 @@ final class InputSchoolLifeInfoIntent: InputSchoolLifeInfoIntentProtocol { model?.updateAuthenticationScore(score: score) } - func hwpFileImporterIsRequred() { - model?.updateIsPresentedHWPFileImporter(isPresented: true) - } - - func hwpFileImporterDismissed() { - model?.updateIsPresentedHWPFileImporter(isPresented: false) - } - - func hwpFileDidSelect(url: URL) { - model?.updateHWPFileURL(url: url) - } - - func failedToImportHWPFile() { - model?.updateErrorField(field: [.hwp]) - } - func prevButtonDidTap() { inputSchoolLifeDelegate?.schoolLifePrevButtonDidTap() } @@ -45,29 +29,14 @@ final class InputSchoolLifeInfoIntent: InputSchoolLifeInfoIntentProtocol { errorSet.insert(.gsmAuthentication) } - if state.hwpFileURL == nil { - errorSet.insert(.hwp) - } - model?.updateErrorField(field: errorSet) guard let gsmScore = Int(state.authenticationScore), - let hwpURL = state.hwpFileURL, - hwpURL.startAccessingSecurityScopedResource(), - let hwpData = try? Data(contentsOf: hwpURL), errorSet.isEmpty else { - state.hwpFileURL?.stopAccessingSecurityScopedResource() return } - hwpURL.stopAccessingSecurityScopedResource() - - let input = InputSchoolLifeInformationObject( - hwpFilename: state.hwpFilename, - gsmAuthenticationScore: gsmScore, - hwpData: hwpData - ) - inputSchoolLifeDelegate?.completeToInputSchoolLife(input: input) + inputSchoolLifeDelegate?.completeToInputSchoolLife(gsmAuthenticationScore: gsmScore) } } diff --git a/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Intent/InputSchoolLifeInfoIntentProtocol.swift b/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Intent/InputSchoolLifeInfoIntentProtocol.swift index 4ee321f3..bed6c79e 100644 --- a/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Intent/InputSchoolLifeInfoIntentProtocol.swift +++ b/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Intent/InputSchoolLifeInfoIntentProtocol.swift @@ -2,10 +2,6 @@ import Foundation protocol InputSchoolLifeInfoIntentProtocol { func updateAuthenticationScore(score: String) - func hwpFileImporterIsRequred() - func hwpFileImporterDismissed() - func hwpFileDidSelect(url: URL) - func failedToImportHWPFile() func prevButtonDidTap() func nextButtonDidTap(state: any InputSchoolLifeInfoStateProtocol) } diff --git a/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Model/InputSchoolLifeInfoModel.swift b/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Model/InputSchoolLifeInfoModel.swift index 42e9e43f..3fadd8e2 100644 --- a/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Model/InputSchoolLifeInfoModel.swift +++ b/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Model/InputSchoolLifeInfoModel.swift @@ -2,23 +2,13 @@ import Foundation enum InputSchoolLifeErrorField: Hashable { case gsmAuthentication - case hwp } final class InputSchoolLifeInfoModel: ObservableObject, InputSchoolLifeInfoStateProtocol { @Published var authenticationScore: String = "" - @Published var isPresentedHWPFileImporter: Bool = false - @Published var hwpFileURL: URL? - var hwpFilename: String { - guard let hwpFileURL else { - return "" - } - return hwpFileURL.lastPathComponent - } @Published var errorField: Set = [] var isDisabledNextButton: Bool { - authenticationScore.isEmpty || - hwpFileURL == nil + authenticationScore.isEmpty } } @@ -27,15 +17,6 @@ extension InputSchoolLifeInfoModel: InputSchoolLifeInfoActionProtocol { self.authenticationScore = score } - func updateIsPresentedHWPFileImporter(isPresented: Bool) { - self.isPresentedHWPFileImporter = isPresented - } - - func updateHWPFileURL(url: URL) { - self.hwpFileURL?.stopAccessingSecurityScopedResource() - self.hwpFileURL = url - } - func updateErrorField(field: Set) { self.errorField = field } diff --git a/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Model/InputSchoolLifeInfoModelProtocol.swift b/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Model/InputSchoolLifeInfoModelProtocol.swift index 1ef108ac..4f126f76 100644 --- a/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Model/InputSchoolLifeInfoModelProtocol.swift +++ b/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Model/InputSchoolLifeInfoModelProtocol.swift @@ -2,16 +2,11 @@ import Foundation protocol InputSchoolLifeInfoStateProtocol { var authenticationScore: String { get } - var isPresentedHWPFileImporter: Bool { get } - var hwpFileURL: URL? { get } - var hwpFilename: String { get } var errorField: Set { get } var isDisabledNextButton: Bool { get } } protocol InputSchoolLifeInfoActionProtocol: AnyObject { func updateAuthenticationScore(score: String) - func updateIsPresentedHWPFileImporter(isPresented: Bool) - func updateHWPFileURL(url: URL) func updateErrorField(field: Set) } diff --git a/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Scene/InputSchoolLifeInfoView.swift b/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Scene/InputSchoolLifeInfoView.swift index 34a2f052..28ee754a 100644 --- a/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Scene/InputSchoolLifeInfoView.swift +++ b/Projects/Feature/InputSchoolLifeInfoFeature/Sources/Scene/InputSchoolLifeInfoView.swift @@ -27,21 +27,6 @@ struct InputSchoolLifeInfoView: View { ) .keyboardType(.numberPad) .titleWrapper("인증제 점수") - - SMSTextField( - "+ hwp 파일 추가", - text: Binding( - get: { state.hwpFilename }, - set: { _ in } - ), - errorText: "hwp, hwpx 확장자인 파일만 가능해요", - isError: state.errorField.contains(.hwp) - ) - .disabled(true) - .titleWrapper("드림북") - .onTapGesture { - intent.hwpFileImporterIsRequred() - } } Spacer() @@ -64,23 +49,5 @@ struct InputSchoolLifeInfoView: View { } } .hideKeyboardWhenTap() - .fileImporter( - isPresented: Binding( - get: { state.isPresentedHWPFileImporter }, - set: { _ in intent.hwpFileImporterDismissed() } - ), - allowedContentTypes: [ - UTType(filenameExtension: "hwp") ?? .pdf, - UTType(filenameExtension: "hwpx") ?? .pdf - ] - ) { result in - switch result { - case let .success(url): - intent.hwpFileDidSelect(url: url) - - case .failure: - intent.failedToImportHWPFile() - } - } } } diff --git a/Projects/Feature/InputSchoolLifeInfoFeature/Tests/InputSchoolLifeInfoFeatureTest.swift b/Projects/Feature/InputSchoolLifeInfoFeature/Tests/InputSchoolLifeInfoFeatureTest.swift index f54d4333..a241f50c 100644 --- a/Projects/Feature/InputSchoolLifeInfoFeature/Tests/InputSchoolLifeInfoFeatureTest.swift +++ b/Projects/Feature/InputSchoolLifeInfoFeature/Tests/InputSchoolLifeInfoFeatureTest.swift @@ -5,7 +5,7 @@ import InputSchoolLifeInfoFeatureInterface final class DummyInputSchoolLifeDelegate: InputSchoolLifeDelegate { func schoolLifePrevButtonDidTap() {} - func completeToInputSchoolLife(input: InputSchoolLifeInfoFeatureInterface.InputSchoolLifeInformationObject) {} + func completeToInputSchoolLife(gsmAuthenticationScore: Int) {} } final class InputSchoolLifeInfoFeatureTests: XCTestCase { @@ -41,26 +41,4 @@ final class InputSchoolLifeInfoFeatureTests: XCTestCase { sut.intent.updateAuthenticationScore(score: "\(randomScore)") XCTAssertEqual(sut.model.authenticationScore, "\(randomScore)") } - - func test_file_importer_sheet() { - sut.intent.hwpFileImporterIsRequred() - XCTAssertTrue(sut.model.isPresentedHWPFileImporter) - - sut.intent.hwpFileImporterDismissed() - XCTAssertFalse(sut.model.isPresentedHWPFileImporter) - } - - func test_hwp_file_select() { - guard let url = URL(string: "localhost:8080/index.html") else { - XCTFail("failed to load url") - return - } - sut.intent.hwpFileDidSelect(url: url) - XCTAssertEqual(sut.model.hwpFileURL, url) - } - - func test_failed_to_import_hwp() { - sut.intent.failedToImportHWPFile() - XCTAssertEqual(sut.model.errorField, [.hwp]) - } } diff --git a/Projects/Feature/MainFeature/Project.swift b/Projects/Feature/MainFeature/Project.swift index c6585593..4eb8bd6f 100644 --- a/Projects/Feature/MainFeature/Project.swift +++ b/Projects/Feature/MainFeature/Project.swift @@ -10,8 +10,8 @@ let project = Project.makeModule( .Feature.BaseFeature, .Feature.StudentDetailFeatureInterface, .Feature.FilterFeatureInterface, + .Feature.MyPageFeatureInterface, .Domain.StudentDomainInterface, - .Domain.AuthDomainInterface, .Domain.UserDomainInterface ] ) diff --git a/Projects/Feature/MainFeature/Sources/DI/MainComponent.swift b/Projects/Feature/MainFeature/Sources/DI/MainComponent.swift index ad7de5e8..3b867d29 100644 --- a/Projects/Feature/MainFeature/Sources/DI/MainComponent.swift +++ b/Projects/Feature/MainFeature/Sources/DI/MainComponent.swift @@ -1,4 +1,3 @@ -import AuthDomainInterface import SwiftUI import BaseFeature import MainFeatureInterface @@ -7,11 +6,12 @@ import StudentDetailFeatureInterface import StudentDomainInterface import FilterFeatureInterface import UserDomainInterface +import MyPageFeatureInterface public protocol MainDependency: Dependency { var studentDomainBuildable: any StudentDomainBuildable { get } - var authDomainBuildable: any AuthDomainBuildable { get } var filterBuildable: any FilterBuildable { get } + var myPageBuildable: any MyPageBuildable { get } var studentDetailBuildable: any StudentDetailBuildable { get } var userDomainBuildable: any UserDomainBuildable { get } } @@ -23,8 +23,6 @@ public final class MainComponent: Component, MainBuildable { model: model, mainDelegate: delegate, fetchStudentListUseCase: dependency.studentDomainBuildable.fetchStudentListUseCase, - logoutUseCase: dependency.authDomainBuildable.logoutUseCase, - withdrawalUseCase: dependency.authDomainBuildable.withdrawalUseCase, loadUserRoleUseCase: dependency.userDomainBuildable.loadUserRoleUseCase ) let container = MVIContainer( @@ -35,7 +33,8 @@ public final class MainComponent: Component, MainBuildable { return MainView( container: container, studentDetailBuildable: dependency.studentDetailBuildable, - filterBuildable: dependency.filterBuildable + filterBuildable: dependency.filterBuildable, + myPageBuildable: dependency.myPageBuildable ) } } diff --git a/Projects/Feature/MainFeature/Sources/Intent/MainIntent.swift b/Projects/Feature/MainFeature/Sources/Intent/MainIntent.swift index 4cf8c730..11dbda20 100644 --- a/Projects/Feature/MainFeature/Sources/Intent/MainIntent.swift +++ b/Projects/Feature/MainFeature/Sources/Intent/MainIntent.swift @@ -1,4 +1,3 @@ -import AuthDomainInterface import FilterFeatureInterface import Combine import MainFeatureInterface @@ -9,23 +8,17 @@ final class MainIntent: MainIntentProtocol { private weak var model: (any MainActionProtocol)? private weak var mainDelegate: (any MainDelegate)? private let fetchStudentListUseCase: any FetchStudentListUseCase - private let logoutUseCase: any LogoutUseCase - private let withdrawalUseCase: any WithdrawalUseCase private let loadUserRoleUseCase: any LoadUserRoleUseCase init( model: any MainActionProtocol, mainDelegate: any MainDelegate, fetchStudentListUseCase: any FetchStudentListUseCase, - logoutUseCase: any LogoutUseCase, - withdrawalUseCase: any WithdrawalUseCase, loadUserRoleUseCase: any LoadUserRoleUseCase ) { self.mainDelegate = mainDelegate self.model = model self.fetchStudentListUseCase = fetchStudentListUseCase - self.logoutUseCase = logoutUseCase - self.withdrawalUseCase = withdrawalUseCase self.loadUserRoleUseCase = loadUserRoleUseCase model.updateUserRole(role: loadUserRoleUseCase.execute()) @@ -65,14 +58,6 @@ final class MainIntent: MainIntentProtocol { } } - func existActionSheetIsRequired() { - model?.updateIsPresentedExistActionSheet(isPresented: true) - } - - func existActionSheetDismissed() { - model?.updateIsPresentedExistActionSheet(isPresented: false) - } - func filterIsRequired() { model?.updateIsPresentedFilterPage(isPresented: true) } @@ -81,52 +66,32 @@ final class MainIntent: MainIntentProtocol { model?.updateIsPresentedFilterPage(isPresented: false) } - func logoutDialogIsRequired() { - model?.updateIsPresentedLogoutDialog(isPresented: true) - } - - func logoutDialogDismissed() { - model?.updateIsPresentedLogoutDialog(isPresented: false) + func myPageIsRequired() { + model?.updateIsPresentedMypage(isPresented: true) } - func logoutDialogIsComplete() { - Task { - do { - try await logoutUseCase.execute() - mainDelegate?.logout() - } catch { - model?.updateIsError(isError: true) - } - } - model?.updateIsPresentedLogoutDialog(isPresented: false) + func myPageDismissed() { + model?.updateIsPresentedMypage(isPresented: false) } - func withdrawalDialogIsRequired() { - model?.updateIsPresentedWithdrawalDialog(isPresented: true) + func studentDidSelect(userID: String) { + model?.updateSelectedUserID(userID: userID) } - func withdrawalDialogDismissed() { - model?.updateIsPresentedWithdrawalDialog(isPresented: false) + func studentDetailDismissed() { + model?.updateSelectedUserID(userID: nil) } - func withdrawalDialogIsComplete() { - Task { - do { - try await withdrawalUseCase.execute() - mainDelegate?.logout() - } catch { - model?.updateIsError(isError: true) - } - } - model?.updateIsPresentedWithdrawalDialog(isPresented: false) + func logout() { + mainDelegate?.logout() } - func studentDidSelect(userID: String) { - model?.updateSelectedUserID(userID: userID) + func exitIsRequired() { + model?.updateIsPresentedExitDialog(isPresented: true) } - func studentDetailDismissed() { - model?.updateSelectedUserID(userID: nil) + func exitIsDismissed() { + model?.updateIsPresentedExitDialog(isPresented: false) } } diff --git a/Projects/Feature/MainFeature/Sources/Intent/MainIntentProtocol.swift b/Projects/Feature/MainFeature/Sources/Intent/MainIntentProtocol.swift index 62de4c81..5dfcaeb6 100644 --- a/Projects/Feature/MainFeature/Sources/Intent/MainIntentProtocol.swift +++ b/Projects/Feature/MainFeature/Sources/Intent/MainIntentProtocol.swift @@ -1,20 +1,17 @@ import Foundation import FilterFeatureInterface +import MyPageFeatureInterface import StudentDomainInterface -protocol MainIntentProtocol: FilterDelegate { +protocol MainIntentProtocol: FilterDelegate, MyPageDelegate { func reachedBottom(page: Int, isLast: Bool, filterOption: FilterOption?) func refresh(filterOption: FilterOption?) - func existActionSheetIsRequired() - func existActionSheetDismissed() func filterIsRequired() func filterDismissed() - func logoutDialogIsRequired() - func logoutDialogDismissed() - func logoutDialogIsComplete() - func withdrawalDialogIsRequired() - func withdrawalDialogDismissed() - func withdrawalDialogIsComplete() + func myPageIsRequired() + func myPageDismissed() func studentDidSelect(userID: String) func studentDetailDismissed() + func exitIsRequired() + func exitIsDismissed() } diff --git a/Projects/Feature/MainFeature/Sources/Model/MainModel.swift b/Projects/Feature/MainFeature/Sources/Model/MainModel.swift index e55d0cd5..e1e83d0d 100644 --- a/Projects/Feature/MainFeature/Sources/Model/MainModel.swift +++ b/Projects/Feature/MainFeature/Sources/Model/MainModel.swift @@ -7,11 +7,8 @@ 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 isRefresh: Bool = false - @Published var isPresentedExistActionSheet: Bool = false - @Published var isPresentedLogoutDialog: Bool = false - @Published var isPresentedWithdrawalDialog: Bool = false + var content: [SingleStudentEntity] { get { _content } set { @@ -22,13 +19,15 @@ final class MainModel: ObservableObject, MainStateProtocol { profileImageURL: $0.profileImageURL, name: $0.name.replacingOccurrences(of: "**", with: "소금"), major: $0.major, - techStack: $0.techStack + techStacks: $0.techStacks ) } } } @Published var _content: [SingleStudentEntity] = [] @Published var isPresentedFilterPage: Bool = false + @Published var isPresentedMyPage: Bool = false + @Published var isPresntedExit: Bool = false @Published var selectedUserID: String? @Published var currentUserRole: UserRoleType = .guest @Published var filterOption: FilterOption? @@ -36,10 +35,6 @@ final class MainModel: ObservableObject, MainStateProtocol { // swiftlint: enable identifier_name extension MainModel: MainActionProtocol { - func updateIsError(isError: Bool) { - self.isError = isError - } - func updatePage(page: Int) { self.page = page } @@ -52,20 +47,16 @@ extension MainModel: MainActionProtocol { self.isLast = isLast } - func updateIsPresentedExistActionSheet(isPresented: Bool) { - self.isPresentedExistActionSheet = isPresented - } - - func updateIsPresentedLogoutDialog(isPresented: Bool) { - self.isPresentedLogoutDialog = isPresented + func updateIsPresentedFilterPage(isPresented: Bool) { + self.isPresentedFilterPage = isPresented } - func updateIsPresentedWithdrawalDialog(isPresented: Bool) { - self.isPresentedWithdrawalDialog = isPresented + func updateIsPresentedMypage(isPresented: Bool) { + self.isPresentedMyPage = isPresented } - func updateIsPresentedFilterPage(isPresented: Bool) { - self.isPresentedFilterPage = isPresented + func updateIsPresentedExitDialog(isPresented: Bool) { + self.isPresntedExit = isPresented } func appendContent(content: [SingleStudentEntity]) { diff --git a/Projects/Feature/MainFeature/Sources/Model/MainModelProtocol.swift b/Projects/Feature/MainFeature/Sources/Model/MainModelProtocol.swift index 9bd007f2..a1c57ced 100644 --- a/Projects/Feature/MainFeature/Sources/Model/MainModelProtocol.swift +++ b/Projects/Feature/MainFeature/Sources/Model/MainModelProtocol.swift @@ -5,12 +5,10 @@ protocol MainStateProtocol { var page: Int { get } var totalSize: Int { get } var isLast: Bool { get } - var isError: Bool { get } var isRefresh: Bool { get } - var isPresentedExistActionSheet: Bool { get } - var isPresentedLogoutDialog: Bool { get } - var isPresentedWithdrawalDialog: Bool { get } var isPresentedFilterPage: Bool { get } + var isPresentedMyPage: Bool { get } + var isPresntedExit: Bool { get } var content: [SingleStudentEntity] { get } var selectedUserID: String? { get } var currentUserRole: UserRoleType { get } @@ -18,14 +16,12 @@ protocol MainStateProtocol { } protocol MainActionProtocol: AnyObject { - func updateIsError(isError: Bool) func updatePage(page: Int) func updateTotalSize(totalSize: Int) func updateIsLast(isLast: Bool) - func updateIsPresentedExistActionSheet(isPresented: Bool) - func updateIsPresentedLogoutDialog(isPresented: Bool) - func updateIsPresentedWithdrawalDialog(isPresented: Bool) func updateIsPresentedFilterPage(isPresented: Bool) + func updateIsPresentedMypage(isPresented: Bool) + func updateIsPresentedExitDialog(isPresented: Bool) func appendContent(content: [SingleStudentEntity]) func updateContent(content: [SingleStudentEntity]) func updateIsRefresh(isRefresh: Bool) diff --git a/Projects/Feature/MainFeature/Sources/Scene/MainView.swift b/Projects/Feature/MainFeature/Sources/Scene/MainView.swift index 03e0a2eb..7a83c2a6 100644 --- a/Projects/Feature/MainFeature/Sources/Scene/MainView.swift +++ b/Projects/Feature/MainFeature/Sources/Scene/MainView.swift @@ -7,6 +7,7 @@ import UIKit import UserDomainInterface import ViewUtil import FilterFeatureInterface +import MyPageFeatureInterface enum MainStudentIDProperty { static let studentScrollToTopID = "STUDENT_SCROLL_TO_TOP" @@ -19,15 +20,18 @@ struct MainView: View { var state: any MainStateProtocol { container.model } private let studentDetailBuildable: any StudentDetailBuildable private let filterBuildable: any FilterBuildable + private let myPageBuildable: any MyPageBuildable init( container: MVIContainer, studentDetailBuildable: any StudentDetailBuildable, - filterBuildable: any FilterBuildable + filterBuildable: any FilterBuildable, + myPageBuildable: any MyPageBuildable ) { self._container = StateObject(wrappedValue: container) self.studentDetailBuildable = studentDetailBuildable self.filterBuildable = filterBuildable + self.myPageBuildable = myPageBuildable } var body: some View { @@ -54,7 +58,7 @@ struct MainView: View { profileImageUrl: item.profileImageURL, name: item.name, major: item.major, - techStack: item.techStack + techStack: item.techStacks ) .foregroundColor(.sms(.system(.black))) .buttonWrapper { @@ -111,6 +115,13 @@ struct MainView: View { set: { _ in intent.filterDismissed() } ) ) + .navigate( + to: myPageBuildable.makeView(delegate: intent).eraseToAnyView(), + when: Binding( + get: { state.isPresentedMyPage }, + set: { _ in intent.myPageDismissed() } + ) + ) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { @@ -123,7 +134,7 @@ struct MainView: View { SMSIcon(.profile, width: 32, height: 32) .clipShape(Circle()) .onTapGesture { - intent.existActionSheetIsRequired() + state.currentUserRole == .student ? intent.myPageIsRequired() : intent.exitIsRequired() } } } @@ -132,77 +143,23 @@ struct MainView: View { SMSIcon(.smsLogo, width: 80, height: 29) } } - .smsBottomSheet(isShowing: Binding( - get: { state.isPresentedExistActionSheet }, - set: { _ in intent.existActionSheetDismissed() } - )) { - VStack(alignment: .leading, spacing: 32) { - Button { - intent.logoutDialogIsRequired() - intent.existActionSheetDismissed() - } label: { - HStack(spacing: 12) { - SMSIcon(.redLogout) - - SMSText("로그아웃", font: .body1) - .foregroundStyle(Color.sms(.error(.e2))) - - Spacer() - } - } - - Button { - intent.withdrawalDialogIsRequired() - intent.existActionSheetDismissed() - } label: { - HStack(spacing: 12) { - SMSIcon(.redPerson) - - SMSText("회원탈퇴", font: .body1) - .foregroundStyle(Color.sms(.error(.e2))) - - Spacer() - } - } - .conditional(state.currentUserRole != .guest) - } - .padding(.top, 12) - .padding(.horizontal, 20) - } - .animation(.default, value: state.isPresentedExistActionSheet) .smsAlert( - title: "로그아웃", - description: "정말로 로그아웃 하시겠습니까?", - isShowing: - Binding( - get: { state.isPresentedLogoutDialog }, - set: { _ in intent.logoutDialogDismissed() } - ), + title: "게스트 종료", + description: "게스트 이용을 종료하시겠습니까?", + isShowing: Binding( + get: { state.isPresntedExit }, + set: { _ in intent.exitIsDismissed() } + ), alertActions: [ - .init(text: "확인", style: .outline) { - intent.logoutDialogIsComplete() + .init(text: "취소", style: .outline) { + intent.exitIsDismissed() }, - .init(text: "취소") { - intent.logoutDialogDismissed() + .init(text: "확인", style: .default) { + intent.exitIsDismissed() + intent.logout() } ] ) - .smsAlert( - title: "회원탈퇴", - description: "정말로 회원탈퇴 하시겠습니까?", - isShowing: - Binding( - get: { state.isPresentedWithdrawalDialog }, - set: { _ in intent.withdrawalDialogDismissed() } - ), - alertActions: [ - .init(text: "확인", style: .outline) { - intent.withdrawalDialogIsComplete() - }, - .init(text: "취소") { - intent.withdrawalDialogDismissed() - } - ]) } .navigationViewStyle(.stack) } diff --git a/Projects/Feature/MyPageFeature/Demo/Resources/LaunchScreen.storyboard b/Projects/Feature/MyPageFeature/Demo/Resources/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Demo/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Feature/MyPageFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/MyPageFeature/Demo/Sources/AppDelegate.swift new file mode 100644 index 00000000..b1d19e75 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Demo/Sources/AppDelegate.swift @@ -0,0 +1,51 @@ +import MyPageFeatureInterface +import SwiftUI +import TechStackAppendFeatureInterface +@testable import AuthDomainTesting +@testable import MyPageFeature +@testable import UserDomainTesting +@testable import FileDomainTesting +@testable import StudentDomainTesting +@testable import MajorDomainTesting + +final class DummyMyPageDelegate: MyPageDelegate { + func logout() {} +} + +struct DummyTechStackAppendBuildable: TechStackAppendBuildable { + func makeView(initial techStacks: [String], completion: @escaping ([String]) -> Void) -> some View { + EmptyView() + } +} + +@main +struct MyPageDemoApp: App { + var body: some Scene { + WindowGroup { + let model = MyPageModel() + let fetchMyProfileUseCase = FetchMyProfileUseCaseSpy() + let logoutUseCase = LogoutUseCaseSpy() + let withdrawalUseCase = WithdrawalUseCaseSpy() + let imageUploadUseCase = ImageUploadUseCaseSpy() + let modifyInformationUseCase = ModifyInformationUseCaseSpy() + let fetchMajorListUseCase = FetchMajorListUseCaseSpy() + let intent = MyPageIntent( + model: model, + myPageDelegate: DummyMyPageDelegate(), + fetchMyProfileUseCase: fetchMyProfileUseCase, + logoutUseCase: logoutUseCase, + withdrawalUseCase: withdrawalUseCase, + imageUploadUseCase: imageUploadUseCase, + modifyInformationUseCase: modifyInformationUseCase, + fetchMajorListUseCase: fetchMajorListUseCase + ) + MyPageView( + container: .init( + intent: intent as MyPageIntentProtocol, + model: model as MyPageStateProtocol, + modelChangePublisher: model.objectWillChange + ), techStackAppendBuildable: DummyTechStackAppendBuildable() + ) + } + } +} diff --git a/Projects/Feature/MyPageFeature/Interface/MyPageBuildable.swift b/Projects/Feature/MyPageFeature/Interface/MyPageBuildable.swift new file mode 100644 index 00000000..578302d0 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Interface/MyPageBuildable.swift @@ -0,0 +1,6 @@ +import SwiftUI + +public protocol MyPageBuildable { + associatedtype ViewType: View + func makeView(delegate: any MyPageDelegate) -> ViewType +} diff --git a/Projects/Feature/MyPageFeature/Interface/MyPageDelegate.swift b/Projects/Feature/MyPageFeature/Interface/MyPageDelegate.swift new file mode 100644 index 00000000..cc674d30 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Interface/MyPageDelegate.swift @@ -0,0 +1,3 @@ +public protocol MyPageDelegate: AnyObject { + func logout() +} diff --git a/Projects/Feature/MyPageFeature/Project.swift b/Projects/Feature/MyPageFeature/Project.swift new file mode 100644 index 00000000..d6db3183 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Project.swift @@ -0,0 +1,23 @@ +import ProjectDescription +import ProjectDescriptionHelpers +import DependencyPlugin + +let project = Project.makeModule( + name: ModulePaths.Feature.MyPageFeature.rawValue, + product: .staticLibrary, + targets: [.interface, .unitTest, .demo], + internalDependencies: [ + .Feature.BaseFeature, + .Feature.TechStackAppendFeatureInterface, + .Domain.UserDomainInterface, + .Domain.StudentDomainInterface, + .Domain.AuthDomainInterface + ], + demoDependencies: [ + .Domain.UserDomainTesting, + .Domain.AuthDomainTesting, + .Domain.FileDomainTesting, + .Domain.StudentDomainTesting, + .Domain.MajorDomainTesting + ] +) diff --git a/Projects/Feature/MyPageFeature/Sources/DI/MyPageComponent.swift b/Projects/Feature/MyPageFeature/Sources/DI/MyPageComponent.swift new file mode 100644 index 00000000..82babb74 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/DI/MyPageComponent.swift @@ -0,0 +1,44 @@ +import AuthDomainInterface +import BaseFeature +import MyPageFeatureInterface +import FileDomainInterface +import StudentDomainInterface +import MajorDomainInterface +import NeedleFoundation +import SwiftUI +import UserDomainInterface +import TechStackAppendFeatureInterface + +public protocol MyPageDependency: Dependency { + var userDomainBuildable: any UserDomainBuildable { get } + var authDomainBuildable: any AuthDomainBuildable { get } + var techStackAppendBuildable: any TechStackAppendBuildable { get } + var fileDomainBuildable: any FileDomainBuildable { get } + var studentDomainBuildable: any StudentDomainBuildable { get } + var majorDomainBuildable: any MajorDomainBuildable { get } +} + +public final class MyPageComponent: Component, MyPageBuildable { + public func makeView(delegate: any MyPageDelegate) -> some View { + let model = MyPageModel() + let intent = MyPageIntent( + model: model, + myPageDelegate: delegate, + fetchMyProfileUseCase: dependency.userDomainBuildable.fetchMyProfileUseCase, + logoutUseCase: dependency.authDomainBuildable.logoutUseCase, + withdrawalUseCase: dependency.authDomainBuildable.withdrawalUseCase, + imageUploadUseCase: dependency.fileDomainBuildable.imageUploadUseCase, + modifyInformationUseCase: dependency.studentDomainBuildable.modifyInformationUseCase, + fetchMajorListUseCase: dependency.majorDomainBuildable.fetchMajorListUseCase + ) + let container = MVIContainer( + intent: intent as MyPageIntentProtocol, + model: model as MyPageStateProtocol, + modelChangePublisher: model.objectWillChange + ) + return MyPageView( + container: container, + techStackAppendBuildable: dependency.techStackAppendBuildable + ) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Intent/MyPageCertificateIntent.swift b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageCertificateIntent.swift new file mode 100644 index 00000000..0483adc4 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageCertificateIntent.swift @@ -0,0 +1,19 @@ +protocol MyPageCertificateIntentProtocol { + func updateCertificate(certificate: String, at index: Int) + func deleteCertificateColumn(at index: Int) + func certificateAppendButtonDidTap() +} + +extension MyPageIntent: MyPageCertificateIntentProtocol { + func updateCertificate(certificate: String, at index: Int) { + model?.updateCertificate(certificate: certificate, at: index) + } + + func deleteCertificateColumn(at index: Int) { + model?.deleteCertificateColumn(at: index) + } + + func certificateAppendButtonDidTap() { + model?.appendCertificate() + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Intent/MyPageIntent.swift b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageIntent.swift new file mode 100644 index 00000000..84eaa11a --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageIntent.swift @@ -0,0 +1,278 @@ +import Foundation +import DesignSystem +import UserDomainInterface +import AuthDomainInterface +import FileDomainInterface +import MyPageFeatureInterface +import StudentDomainInterface +import MajorDomainInterface +import ConcurrencyUtil +import DateUtil + +final class MyPageIntent: MyPageIntentProtocol { + weak var model: (any MyPageActionProtocol)? + private weak var myPageDelegate: (any MyPageDelegate)? + private let fetchMyProfileUseCase: any FetchMyProfileUseCase + private let logoutUseCase: any LogoutUseCase + private let withdrawalUseCase: any WithdrawalUseCase + private let imageUploadUseCase: any ImageUploadUseCase + private let modifyInformationUseCase: any ModifyInformationUseCase + private let fetchMajorListUseCase: any FetchMajorListUseCase + + init( + model: any MyPageActionProtocol, + myPageDelegate: any MyPageDelegate, + fetchMyProfileUseCase: any FetchMyProfileUseCase, + logoutUseCase: any LogoutUseCase, + withdrawalUseCase: any WithdrawalUseCase, + imageUploadUseCase: any ImageUploadUseCase, + modifyInformationUseCase: any ModifyInformationUseCase, + fetchMajorListUseCase: any FetchMajorListUseCase + ) { + self.model = model + self.myPageDelegate = myPageDelegate + self.fetchMyProfileUseCase = fetchMyProfileUseCase + self.logoutUseCase = logoutUseCase + self.withdrawalUseCase = withdrawalUseCase + self.imageUploadUseCase = imageUploadUseCase + self.modifyInformationUseCase = modifyInformationUseCase + self.fetchMajorListUseCase = fetchMajorListUseCase + } + + func onAppear() { + Task { + do { + let majorList = try await fetchMajorListUseCase.execute() + model?.updateMajorList(majorList: majorList) + let profile = try await fetchMyProfileUseCase.execute() + model?.updateProfileURL(url: profile.profileImageURL) + model?.updateIntroduce(introduce: profile.introduce) + model?.updatePortfolioURL(portfolioURL: profile.portfolioURL) + model?.updateMajor(major: profile.major) + model?.updateEmail(email: profile.contactEmail) + model?.updateGSMScore(gsmScore: "\(profile.gsmAuthenticationScore)") + model?.updateFormOfEmployment(form: profile.formOfEmployment) + model?.updateWorkRegions(regions: profile.regions) + model?.updateMilitaryServiceType(type: profile.militaryService) + model?.updateSalary(salary: "\(profile.salary)") + model?.updateLanguageScores( + languages: profile.languageCertificates.map { + $0.toModel() + } + ) + model?.updateCertificates(certificates: profile.certificates) + model?.updateTechStacks(techStacks: profile.techStacks) + model?.updateProjectList( + projectList: profile.proejcts.map { + $0.toModel() + } + ) + model?.updatePrizeList( + prizeList: profile.prizes.map { + $0.toModel() + } + ) + } catch { + model?.updateIsError(isError: true) + } + } + } + + func exitActionSheetIsRequired() { + model?.updateIsPresentedExitBottomSheet(isPresented: true) + } + + func exitActionSheetDismissed() { + model?.updateIsPresentedExitBottomSheet(isPresented: false) + } + + func logoutDialogIsRequired() { + model?.updateIsPresentedLogoutDialog(isPresented: true) + } + + func logoutDialogDismissed() { + model?.updateIsPresentedLogoutDialog(isPresented: false) + } + + func logoutDialogIsComplete() { + Task { + do { + try await logoutUseCase.execute() + myPageDelegate?.logout() + } catch { + model?.updateIsError(isError: true) + } + } + model?.updateIsPresentedLogoutDialog(isPresented: false) + } + + func withdrawalDialogIsRequired() { + model?.updateIsPresentedWithdrawalDialog(isPresented: true) + } + + func withdrawalDialogDismissed() { + model?.updateIsPresentedWithdrawalDialog(isPresented: false) + } + + func withdrawalDialogIsComplete() { + Task { + do { + try await withdrawalUseCase.execute() + myPageDelegate?.logout() + } catch { + model?.updateIsError(isError: true) + } + } + model?.updateIsPresentedWithdrawalDialog(isPresented: false) + } + + func modifyToInputAllInfo(state: any MyPageStateProtocol) { + Task { + do { + let modifyInformationRequest = ModifyStudentInformationRequestDTO( + certificates: state.certificates, + contactEmail: state.email, + formOfEmployment: FormOfEmployment(rawValue: state.formOfEmployment.rawValue) ?? .fullTime, + gsmAuthenticationScore: Int(state.gsmScore) ?? 0, + introduce: state.introduce, + languageCertificates: state.languageList.map { $0.toDTO() }, + major: state.major, + militaryService: state.selectedMilitaryServiceType, + portfolioURL: state.portfolioURL, + profileImgURL: state.profileURL, + regions: state.workRegionList, + salary: Int(state.salary) ?? 0, + techStacks: state.techStacks, + projects: state.projectList.map { + let startAtString = $0.startAt.toStringCustomFormat(format: "yyyy.MM") + let endAtString = $0.endAt?.toStringCustomFormat(format: "yyyy.MM") ?? "개발중" + + return $0.toDTO( + iconURL: $0.iconImage, + previewImageURLS: $0.previewImages, + startAt: startAtString, + endAt: endAtString + ) + }, + prizes: state.prizeList.map { $0.toDTO() } + ) + + try await modifyInformationUseCase.execute(req: modifyInformationRequest) + } catch { + model?.updateIsError(isError: true) + } + } + } + + func imageUpload(imageResult: PickedImageResult) async throws -> String { + try await Task { + try await imageUploadUseCase.execute( + image: imageResult.uiImage.jpegData(compressionQuality: 0.2) ?? .init(), + fileName: imageResult.fileName + ) + } + .value + } +} + +extension LanguageModel { + func toDTO() -> ModifyStudentInformationRequestDTO.LanguageCertificate { + ModifyStudentInformationRequestDTO.LanguageCertificate( + languageCertificateName: name, + score: score + ) + } +} + +extension ProjectModel { + func toDTO( + iconURL: String, + previewImageURLS: [String], + startAt: String, + endAt: String + ) -> ModifyStudentInformationRequestDTO.Project { + ModifyStudentInformationRequestDTO.Project( + name: name, + iconImageURL: iconURL, + previewImageURLs: previewImageURLS, + description: content, + links: relatedLinks.map { $0.toDTO() }, + techStacks: Array(techStacks), + myActivity: mainTask, + inProgress: .init( + start: startAt, + end: endAt + ) + ) + } +} + +extension ProjectModel.RelatedLink { + func toDTO() -> ModifyStudentInformationRequestDTO.Project.Link { + ModifyStudentInformationRequestDTO.Project.Link( + name: name, + url: url + ) + } +} + +extension PrizeModel { + func toDTO() -> ModifyStudentInformationRequestDTO.Prize { + ModifyStudentInformationRequestDTO.Prize( + name: name, + type: prize, + date: prizeAtString + ) + } +} + +extension ProjectEntity { + func toModel() -> ProjectModel { + ProjectModel.init( + name: name, + iconImage: iconImageURL, + previewImages: previewImageURLs, + content: description, + techStacks: Set(techStacks), + mainTask: myActivity, + startAt: inProgress.start.toISODate( + timeZone: .init(identifier: "GMT") ?? .current + ), + endAt: inProgress.end?.toISODate( + timeZone: .init(identifier: "GMT") ?? .current + ), + isInProgress: ((inProgress.end?.isEmpty) != nil), + relatedLinks: links.map { $0.toModel() } + ) + } +} + +extension ProjectEntity.LinkEntity { + func toModel() -> ProjectModel.RelatedLink { + ProjectModel.RelatedLink.init( + name: name, + url: url + ) + } +} + +extension PrizeEntity { + func toModel() -> PrizeModel { + PrizeModel.init( + name: name, + prize: type, + prizeAt: date.toISODate( + timeZone: .init(identifier: "GMT") ?? .current + ) + ) + } +} + +extension LanguageCertificateEntity { + func toModel() -> LanguageModel { + LanguageModel.init( + name: name, + score: score + ) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Intent/MyPageIntentProtocol.swift b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageIntentProtocol.swift new file mode 100644 index 00000000..04332a61 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageIntentProtocol.swift @@ -0,0 +1,22 @@ +import Foundation + +protocol MyPageIntentProtocol: + MyPageProfileIntentProtocol, + MyPageSchoolLifeIntentProtocol, + MyPageWorkInfoIntentProtocol, + MyPageMilitaryIntentProtocol, + MyPageCertificateIntentProtocol, + MyPageLanguageInfoIntentProtocol, + MyPageProjectIntentProtocol, + MyPagePrizeIntentProtocol { + func onAppear() + func exitActionSheetIsRequired() + func exitActionSheetDismissed() + func logoutDialogIsRequired() + func logoutDialogDismissed() + func logoutDialogIsComplete() + func withdrawalDialogIsRequired() + func withdrawalDialogDismissed() + func withdrawalDialogIsComplete() + func modifyToInputAllInfo(state: MyPageStateProtocol) +} diff --git a/Projects/Feature/MyPageFeature/Sources/Intent/MyPageLanguageIntent.swift b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageLanguageIntent.swift new file mode 100644 index 00000000..57c44e6a --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageLanguageIntent.swift @@ -0,0 +1,24 @@ +protocol MyPageLanguageInfoIntentProtocol { + func updateLanguageName(name: String, at index: Int) + func updateLanguageScore(score: String, at index: Int) + func deleteLanguage(at index: Int) + func languageAppendButtonDidTap() +} + +extension MyPageIntent: MyPageLanguageInfoIntentProtocol { + func updateLanguageName(name: String, at index: Int) { + model?.updateLanguageName(name: name, at: index) + } + + func updateLanguageScore(score: String, at index: Int) { + model?.updateLanguageScore(score: score, at: index) + } + + func deleteLanguage(at index: Int) { + model?.deleteLanguage(at: index) + } + + func languageAppendButtonDidTap() { + model?.appendLanguage() + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Intent/MyPageMilitaryIntent.swift b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageMilitaryIntent.swift new file mode 100644 index 00000000..6e66691e --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageMilitaryIntent.swift @@ -0,0 +1,21 @@ +import StudentDomainInterface + +protocol MyPageMilitaryIntentProtocol { + func militarySheetIsRequired() + func militarySheetDismissed() + func militaryServiceTypeDidSelected(type: MilitaryServiceType) +} + +extension MyPageIntent: MyPageMilitaryIntentProtocol { + func militarySheetIsRequired() { + model?.updateIsPresentedMilitarySheet(isPresented: true) + } + + func militarySheetDismissed() { + model?.updateIsPresentedMilitarySheet(isPresented: false) + } + + func militaryServiceTypeDidSelected(type: MilitaryServiceType) { + model?.updateMilitaryServiceType(type: type) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Intent/MyPagePrizeIntentProtocol.swift b/Projects/Feature/MyPageFeature/Sources/Intent/MyPagePrizeIntentProtocol.swift new file mode 100644 index 00000000..b7aa4551 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Intent/MyPagePrizeIntentProtocol.swift @@ -0,0 +1,62 @@ +import Foundation + +protocol MyPagePrizeIntentProtocol { + func prizeToggleButtonDidTap(index: Int) + func prizeRemoveButtonDidTap(index: Int) + func updatePrizeName(index: Int, name: String) + func updatePrizePrize(index: Int, prize: String) + func updatePrizePrizeAt(index: Int, prizeAt: Date) + func prizePrizeAtDidSelect(index: Int, prizeAt: Date) + func appendEmptyPrize() + func removePrize(index: Int) + func prizeAtButtonDidTap(index: Int) + func prizeAtDismissed() + func prizeAppendButtonDidTap() +} + +extension MyPageIntent: MyPagePrizeIntentProtocol { + func prizeToggleButtonDidTap(index: Int) { + model?.toggleCollapsedPrize(index: index) + } + + func updatePrizeName(index: Int, name: String) { + model?.updatePrizeName(index: index, name: name) + } + + func updatePrizePrize(index: Int, prize: String) { + model?.updatePrizePrize(index: index, prize: prize) + } + + func updatePrizePrizeAt(index: Int, prizeAt: Date) { + model?.updatePrizePrizeAt(index: index, prizeAt: prizeAt) + } + + func prizePrizeAtDidSelect(index: Int, prizeAt: Date) { + model?.updatePrizePrizeAt(index: index, prizeAt: prizeAt) + } + + func prizeRemoveButtonDidTap(index: Int) { + model?.removePrize(index: index) + } + + func appendEmptyPrize() { + model?.appendEmptyPrize() + } + + func removePrize(index: Int) { + model?.removePrize(index: index) + } + + func prizeAtButtonDidTap(index: Int) { + model?.updateFocusedPrizeIndex(index: index) + model?.updateIsPresentedPrizeAtDatePicker(isPresented: true) + } + + func prizeAtDismissed() { + model?.updateIsPresentedPrizeAtDatePicker(isPresented: false) + } + + func prizeAppendButtonDidTap() { + model?.appendEmptyPrize() + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Intent/MyPageProfileIntent.swift b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageProfileIntent.swift new file mode 100644 index 00000000..5c176b9d --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageProfileIntent.swift @@ -0,0 +1,108 @@ +import DesignSystem + +protocol MyPageProfileIntentProtocol { + func updateIntroduce(introduce: String) + func updateEmail(email: String) + func updateMajor(major: String) + func updatePortfolioURL(portfolioURL: String) + func techStackAppendIsRequired() + func techStackAppendDismissed() + func techStackAppendDidComplete(techStacks: [String]) + func removeTechStack(techStack: String) + func majorSheetIsRequired() + func majorSheetDismissed() + func imagePickerIsRequired() + func imagePickerDismissed() + func imageMethodPickerIsRequired() + func imageMethodPickerDismissed() + func imageDidSelected(imageResult: PickedImageResult?) + func cameraIsRequired() + func cameraDismissed() + func activeSelfEntering() + func deActiveSelfEntering() +} + +extension MyPageIntent: MyPageProfileIntentProtocol { + func updateIntroduce(introduce: String) { + model?.updateIntroduce(introduce: introduce) + } + + func updateEmail(email: String) { + model?.updateEmail(email: email) + } + + func updateMajor(major: String) { + model?.updateMajor(major: major) + } + + func updatePortfolioURL(portfolioURL: String) { + model?.updatePortfolioURL(portfolioURL: portfolioURL) + } + + func techStackAppendIsRequired() { + model?.updateIsPresentedTechStackAppend(isPresented: true) + } + + func techStackAppendDismissed() { + model?.updateIsPresentedTechStackAppend(isPresented: false) + } + + func techStackAppendDidComplete(techStacks: [String]) { + model?.updateTechStacks(techStacks: techStacks) + } + + func removeTechStack(techStack: String) { + model?.removeTechStack(techStack: techStack) + } + + func majorSheetIsRequired() { + model?.updateIsPresentedMajorSheet(isPresented: true) + } + + func majorSheetDismissed() { + model?.updateIsPresentedMajorSheet(isPresented: false) + } + + func imagePickerIsRequired() { + model?.updateIsPresentedProfileImage(isPresented: true) + } + + func imagePickerDismissed() { + model?.updateIsPresentedProfileImage(isPresented: false) + } + + func imageMethodPickerIsRequired() { + model?.updateIsPresentedImageMethodPicker(isPresented: true) + } + + func imageMethodPickerDismissed() { + model?.updateIsPresentedImageMethodPicker(isPresented: false) + } + + func imageDidSelected(imageResult: PickedImageResult?) { + guard let imageResult else { return } + Task { + do { + async let profileURL = imageUpload(imageResult: imageResult) + + try await model?.updateProfileURL(url: profileURL) + } + } + } + + func cameraIsRequired() { + model?.updateIsPresentedProfileCamera(isPresented: true) + } + + func cameraDismissed() { + model?.updateIsPresentedProfileCamera(isPresented: false) + } + + func activeSelfEntering() { + model?.updateIsSelfEntering(isSelfEntering: true) + } + + func deActiveSelfEntering() { + model?.updateIsSelfEntering(isSelfEntering: false) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Intent/MyPageProjectIntent.swift b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageProjectIntent.swift new file mode 100644 index 00000000..5b21ec10 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageProjectIntent.swift @@ -0,0 +1,171 @@ +import DesignSystem +import Foundation + +protocol MyPageProjectIntentProtocol { + func projectToggleButtonDidTap(index: Int) + func updateProjectName(index: Int, name: String) + func updateIconImage(index: Int, image: PickedImageResult) + func appendPreviewImageButtonDidTap(index: Int, previewsCount: Int) + func appendPreviewImage(index: Int, image: PickedImageResult) + func removePreviewImageDidTap(index: Int, previewIndex: Int) + func updateProjectContent(index: Int, content: String) + func techStacksDidSelect(index: Int, techStacks: [String]) + func removeProjectTechStackButtonDidTap(index: Int, techStack: String) + func updateProjectMainTask(index: Int, mainTask: String) + func projectStartAtButtonDidTap(index: Int) + func projectEndAtButtonDidTap(index: Int) + func projectStartAtDidSelect(index: Int, startAt: Date) + func projectEndAtDidSelect(index: Int, endAt: Date) + func projectIsInProgressButtonDidTap(index: Int, isInProgress: Bool) + func updateProjectLinkName(index: Int, linkIndex: Int, name: String) + func updateProjectLinkURL(index: Int, linkIndex: Int, url: String) + func relatedLinkAppendButtonDidTap(index: Int) + func removeProjectRelatedLinkDidTap(index: Int, linkIndex: Int) + func projectAppendButtonDidTap() + func projectRemoveButtonDidTap(index: Int) + func projectIconImageButtonDidTap(index: Int) + func projectIconImagePickerDismissed() + func projectPreviewImagePickerDismissed() + func projectStartAtDatePickerDismissed() + func projectEndAtDatePickerDismissed() + func projectTechStackAppendButtonDidTap(index: Int) + func projectTechStackAppendDismissed() + func projectToastDismissed() +} + +extension MyPageIntent: MyPageProjectIntentProtocol { + func projectToggleButtonDidTap(index: Int) { + model?.toggleCollapsedProject(index: index) + } + + func updateProjectName(index: Int, name: String) { + model?.updateProjectName(index: index, name: name) + } + + func updateIconImage(index: Int, image: PickedImageResult) { + Task { + do { + async let iconImageURL = imageUpload(imageResult: image) + try await model?.updateIconImage(index: index, imageURL: iconImageURL) + } + } + } + + func appendPreviewImageButtonDidTap(index: Int, previewsCount: Int) { + if previewsCount == 4 { + model?.updateIsPresentedProjectToast(isPresented: true) + } else { + model?.updateFocusedProjectIndex(index: index) + model?.updateIsPresentedPreviewImagePicker(isPresented: true) + } + } + + func appendPreviewImage(index: Int, image: PickedImageResult) { + Task { + do { + async let previewImageURL = imageUpload(imageResult: image) + try await model?.appendPreviewImage(index: index, imageURL: previewImageURL) + } + } + } + + func removePreviewImageDidTap(index: Int, previewIndex: Int) { + model?.removePreviewImage(index: index, previewIndex: previewIndex) + } + + func updateProjectContent(index: Int, content: String) { + model?.updateProjectContent(index: index, content: content) + } + + func techStacksDidSelect(index: Int, techStacks: [String]) { + model?.updateProjectTechStacks(index: index, techStacks: techStacks) + } + + func removeProjectTechStackButtonDidTap(index: Int, techStack: String) { + model?.removeProjectTechStack(index: index, techStack: techStack) + } + + func updateProjectMainTask(index: Int, mainTask: String) { + model?.updateProjectMainTask(index: index, mainTask: mainTask) + } + + func projectStartAtButtonDidTap(index: Int) { + model?.updateFocusedProjectIndex(index: index) + model?.updateIsPresentedProjectStartAtDatePicker(isPresented: true) + } + + func projectEndAtButtonDidTap(index: Int) { + model?.updateFocusedProjectIndex(index: index) + model?.updateIsPresentedProjectEndAtDatePicker(isPresented: true) + } + + func projectIsInProgressButtonDidTap(index: Int, isInProgress: Bool) { + model?.updateIsInProgress(index: index, isInProgress: isInProgress) + } + + func projectStartAtDidSelect(index: Int, startAt: Date) { + model?.updateProjectStartAt(index: index, startAt: startAt) + } + + func projectEndAtDidSelect(index: Int, endAt: Date) { + model?.updateProjectEndAt(index: index, endAt: endAt) + } + + func updateProjectLinkName(index: Int, linkIndex: Int, name: String) { + model?.updateProjectLinkName(index: index, linkIndex: linkIndex, name: name) + } + + func updateProjectLinkURL(index: Int, linkIndex: Int, url: String) { + model?.updateProjectLinkURL(index: index, linkIndex: linkIndex, url: url) + } + + func relatedLinkAppendButtonDidTap(index: Int) { + model?.appendEmptyRelatedLink(index: index) + } + + func removeProjectRelatedLinkDidTap(index: Int, linkIndex: Int) { + model?.removeProjectRelatedLink(index: index, linkIndex: linkIndex) + } + + func projectAppendButtonDidTap() { + model?.appendEmptyProject() + } + + func projectRemoveButtonDidTap(index: Int) { + model?.removeProject(index: index) + } + + func projectIconImageButtonDidTap(index: Int) { + model?.updateFocusedProjectIndex(index: index) + model?.updateIsPresentedProjectImagePicker(isPresented: true) + } + + func projectIconImagePickerDismissed() { + model?.updateIsPresentedProjectImagePicker(isPresented: false) + } + + func projectPreviewImagePickerDismissed() { + model?.updateIsPresentedPreviewImagePicker(isPresented: false) + } + + func projectStartAtDatePickerDismissed() { + model?.updateIsPresentedProjectStartAtDatePicker(isPresented: false) + } + + func projectEndAtDatePickerDismissed() { + model?.updateIsPresentedProjectEndAtDatePicker(isPresented: false) + } + + func projectTechStackAppendButtonDidTap(index: Int) { + model?.updateFocusedProjectIndex(index: index) + model?.updateIsPresentedProjectTechStackAppend(isPresented: true) + } + + func projectTechStackAppendDismissed() { + model?.updateIsPresentedProjectTechStackAppend(isPresented: false) + } + + func projectToastDismissed() { + model?.updateIsPresentedProjectToast(isPresented: false) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Intent/MyPageSchoolLifeIntent.swift b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageSchoolLifeIntent.swift new file mode 100644 index 00000000..44d97533 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageSchoolLifeIntent.swift @@ -0,0 +1,9 @@ +protocol MyPageSchoolLifeIntentProtocol { + func updateGSMScore(gsmScore: String) +} + +extension MyPageIntent: MyPageSchoolLifeIntentProtocol { + func updateGSMScore(gsmScore: String) { + model?.updateGSMScore(gsmScore: gsmScore) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Intent/MyPageWorkInfoIntent.swift b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageWorkInfoIntent.swift new file mode 100644 index 00000000..cd50dc2f --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Intent/MyPageWorkInfoIntent.swift @@ -0,0 +1,41 @@ +import StudentDomainInterface + +protocol MyPageWorkInfoIntentProtocol { + func appendWorkRegion() + func updateWorkRegion(region: String, at index: Int) + func deleteWorkRegion(at index: Int) + func updateSalary(salary: String) + func updateFormOfEmployment(form: FormOfEmployment) + func formOfEmployeementSheetIsRequired() + func formOfEmployeementSheetDismissed() +} + +extension MyPageIntent: MyPageWorkInfoIntentProtocol { + func appendWorkRegion() { + model?.appendWorkRegion() + } + + func updateWorkRegion(region: String, at index: Int) { + model?.updateWorkRegion(region: region, at: index) + } + + func deleteWorkRegion(at index: Int) { + model?.deleteWorkRegion(at: index) + } + + func updateSalary(salary: String) { + model?.updateSalary(salary: salary) + } + + func updateFormOfEmployment(form: FormOfEmployment) { + model?.updateFormOfEmployment(form: form) + } + + func formOfEmployeementSheetIsRequired() { + model?.updateIsPresentedFormOfEmployeementSheet(isPresented: true) + } + + func formOfEmployeementSheetDismissed() { + model?.updateIsPresentedFormOfEmployeementSheet(isPresented: false) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Model/MyPageCertificateModel.swift b/Projects/Feature/MyPageFeature/Sources/Model/MyPageCertificateModel.swift new file mode 100644 index 00000000..e2538383 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Model/MyPageCertificateModel.swift @@ -0,0 +1,31 @@ +import FoundationUtil + +protocol MyPageCertificateStateProtocol { + var certificates: [String] { get } +} + +protocol MyPageCertificateActionProtocol: AnyObject { + func updateCertificate(certificate: String, at index: Int) + func updateCertificates(certificates: [String]) + func deleteCertificateColumn(at index: Int) + func appendCertificate() +} + +extension MyPageModel: MyPageCertificateActionProtocol { + func updateCertificate(certificate: String, at index: Int) { + guard certificates[safe: index] != nil else { return } + certificates[index] = certificate + } + + func updateCertificates(certificates: [String]) { + self.certificates = certificates + } + + func deleteCertificateColumn(at index: Int) { + certificates.remove(at: index) + } + + func appendCertificate() { + certificates.append("") + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Model/MyPageLanguageModel.swift b/Projects/Feature/MyPageFeature/Sources/Model/MyPageLanguageModel.swift new file mode 100644 index 00000000..ceab066d --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Model/MyPageLanguageModel.swift @@ -0,0 +1,42 @@ +import StudentDomainInterface + +struct LanguageModel: Equatable { + var name: String + var score: String +} + +protocol MyPageLanguageInfoStateProtocol { + var languageList: [LanguageModel] { get } +} + +protocol MyPageLanguageInfoActionProtocol: AnyObject { + func updateLanguageName(name: String, at index: Int) + func updateLanguageScore(score: String, at index: Int) + func updateLanguageScores(languages: [LanguageModel]) + func deleteLanguage(at index: Int) + func appendLanguage() +} + +extension MyPageModel: MyPageLanguageInfoActionProtocol { + func updateLanguageName(name: String, at index: Int) { + guard languageList[safe: index] != nil else { return } + languageList[index].name = name + } + + func updateLanguageScore(score: String, at index: Int) { + guard languageList[safe: index] != nil else { return } + languageList[index].score = score + } + + func updateLanguageScores(languages: [LanguageModel]) { + self.languageList = languages + } + + func deleteLanguage(at index: Int) { + languageList.remove(at: index) + } + + func appendLanguage() { + languageList.append(LanguageModel(name: "", score: "")) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Model/MyPageMilitaryModel.swift b/Projects/Feature/MyPageFeature/Sources/Model/MyPageMilitaryModel.swift new file mode 100644 index 00000000..24987daa --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Model/MyPageMilitaryModel.swift @@ -0,0 +1,21 @@ +import StudentDomainInterface + +protocol MyPageMilitaryStateProtocol { + var selectedMilitaryServiceType: MilitaryServiceType { get } + var isPresentedMilitarySheet: Bool { get } +} + +protocol MyPageMilitaryActionProtocol: AnyObject { + func updateIsPresentedMilitarySheet(isPresented: Bool) + func updateMilitaryServiceType(type: MilitaryServiceType) +} + +extension MyPageModel: MyPageMilitaryActionProtocol { + func updateIsPresentedMilitarySheet(isPresented: Bool) { + self.isPresentedMilitarySheet = isPresented + } + + func updateMilitaryServiceType(type: MilitaryServiceType) { + self.selectedMilitaryServiceType = type + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Model/MyPageModel.swift b/Projects/Feature/MyPageFeature/Sources/Model/MyPageModel.swift new file mode 100644 index 00000000..99272fe8 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Model/MyPageModel.swift @@ -0,0 +1,80 @@ +import Foundation +import StudentDomainInterface + +final class MyPageModel: ObservableObject, MyPageStateProtocol { + // MARK: MyPage + @Published var isError: Bool = false + @Published var isPresentedExitBottomSheet: Bool = false + @Published var isPresentedLogoutDialog: Bool = false + @Published var isPresentedWithdrawalDialog: Bool = false + + // MARK: Profile + @Published var profileURL: String = "" + @Published var introduce: String = "" + @Published var email: String = "" + @Published var major: String = "" + @Published var majorList: [String] = [] + @Published var portfolioURL: String = "" + @Published var techStacks: [String] = [] + @Published var isSelfEntering: Bool = false + @Published var isPresentedMajorSheet: Bool = false + @Published var isPresentedImageMethodPicker: Bool = false + @Published var isPresentedProfileCamera: Bool = false + @Published var isPresentedProfileImage: Bool = false + @Published var isPresentedTechStackAppend: Bool = false + + // MARK: SchoolLife + @Published var gsmScore: String = "" + + // MARK: WorkInfo + @Published var workRegionList: [String] = [] + @Published var salary: String = "" + @Published var salaryDisplay: String = "" + @Published var formOfEmployment: FormOfEmployment = .fullTime + @Published var isPresentedFormOfEmployeementSheet: Bool = false + + // MARK: Military + @Published var selectedMilitaryServiceType: MilitaryServiceType = .hope + @Published var isPresentedMilitarySheet: Bool = false + + // MARK: Certificate + @Published var certificates: [String] = [] + + // MARK: Language + @Published var languageList: [LanguageModel] = [] + + // MARK: Project + @Published var projectList: [ProjectModel] = [] + @Published var collapsedProject: [Bool] = [] + @Published var focusedProjectIndex: Int = 0 + @Published var isPresentedProjectImagePicker: Bool = false + @Published var isPresentedPreviewImagePicker: Bool = false + @Published var isPresentedProjectStartAtDatePicker: Bool = false + @Published var isPresentedProjectEndAtDatePicker: Bool = false + @Published var isPresentedProjectTechStackAppend: Bool = false + @Published var isPresentedProjectToast: Bool = false + + // MARK: Prize + @Published var prizeList: [PrizeModel] = [] + @Published var collapsedPrize: [Bool] = [] + @Published var isPresentedPrizeAtDatePicker: Bool = false + @Published var focusedPrizeIndex: Int = 0 +} + +extension MyPageModel: MyPageActionProtocol { + func updateIsError(isError: Bool) { + self.isError = isError + } + + func updateIsPresentedExitBottomSheet(isPresented: Bool) { + self.isPresentedExitBottomSheet = isPresented + } + + func updateIsPresentedLogoutDialog(isPresented: Bool) { + self.isPresentedLogoutDialog = isPresented + } + + func updateIsPresentedWithdrawalDialog(isPresented: Bool) { + self.isPresentedWithdrawalDialog = isPresented + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Model/MyPageModelProtocol.swift b/Projects/Feature/MyPageFeature/Sources/Model/MyPageModelProtocol.swift new file mode 100644 index 00000000..1e405191 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Model/MyPageModelProtocol.swift @@ -0,0 +1,30 @@ +protocol MyPageStateProtocol: + MyPageProfileStateProtocol, + MyPageSchoolLifeStateProtocol, + MyPageWorkInfoStateProtocol, + MyPageMilitaryStateProtocol, + MyPageCertificateStateProtocol, + MyPageLanguageInfoStateProtocol, + MyPageProjectStateProtocol, + MyPagePrizeStateProtocol { + var isError: Bool { get } + var isPresentedExitBottomSheet: Bool { get } + var isPresentedLogoutDialog: Bool { get } + var isPresentedWithdrawalDialog: Bool { get } +} + +protocol MyPageActionProtocol: + AnyObject, + MyPageProfileActionProtocol, + MyPageSchoolLifeActionProtocol, + MyPageWorkInfoActionProtocol, + MyPageMilitaryActionProtocol, + MyPageCertificateActionProtocol, + MyPageLanguageInfoActionProtocol, + MyPageProjectActionProtocol, + MyPagePrizeActionProtocol { + func updateIsError(isError: Bool) + func updateIsPresentedExitBottomSheet(isPresented: Bool) + func updateIsPresentedLogoutDialog(isPresented: Bool) + func updateIsPresentedWithdrawalDialog(isPresented: Bool) +} diff --git a/Projects/Feature/MyPageFeature/Sources/Model/MyPagePrizeModel.swift b/Projects/Feature/MyPageFeature/Sources/Model/MyPagePrizeModel.swift new file mode 100644 index 00000000..b0611d3a --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Model/MyPagePrizeModel.swift @@ -0,0 +1,82 @@ +import Foundation +import DateUtil + +struct PrizeModel: Equatable { + var name: String + var prize: String + var prizeAt: Date + + var prizeAtString: String { + prizeAt.toStringCustomFormat(format: "yyyy.MM") + } +} + +protocol MyPagePrizeStateProtocol { + var prizeList: [PrizeModel] { get } + var collapsedPrize: [Bool] { get } + var isPresentedPrizeAtDatePicker: Bool { get } + var focusedPrizeIndex: Int { get } +} + +protocol MyPagePrizeActionProtocol: AnyObject { + func toggleCollapsedPrize(index: Int) + func updatePrizeList(prizeList: [PrizeModel]) + func updatePrizeName(index: Int, name: String) + func updatePrizePrize(index: Int, prize: String) + func updatePrizePrizeAt(index: Int, prizeAt: Date) + func appendEmptyPrize() + func removePrize(index: Int) + func updateFocusedPrizeIndex(index: Int) + func updateIsPresentedPrizeAtDatePicker(isPresented: Bool) +} + +extension MyPageModel: MyPagePrizeActionProtocol { + func toggleCollapsedPrize(index: Int) { + guard collapsedPrize[safe: index] != nil else { return } + self.collapsedPrize[index].toggle() + } + + func updatePrizeList(prizeList: [PrizeModel]) { + self.prizeList = prizeList + } + + func updatePrizeName(index: Int, name: String) { + guard prizeList[safe: index] != nil else { return } + self.prizeList[index].name = name + } + + func updatePrizePrize(index: Int, prize: String) { + guard prizeList[safe: index] != nil else { return } + self.prizeList[index].prize = prize + } + + func updatePrizePrizeAt(index: Int, prizeAt: Date) { + guard prizeList[safe: index] != nil else { return } + self.prizeList[index].prizeAt = prizeAt + } + + func appendEmptyPrize() { + let newPrize = PrizeModel( + name: "", + prize: "", + prizeAt: Date() + ) + self.prizeList.append(newPrize) + self.collapsedPrize.append(false) + } + + func removePrize(index: Int) { + guard prizeList[safe: index] != nil, collapsedPrize[safe: index] != nil else { return } + self.prizeList.remove(at: index) + self.collapsedPrize.remove(at: index) + } + + func updateFocusedPrizeIndex(index: Int) { + self.focusedPrizeIndex = index + } + + func updateIsPresentedPrizeAtDatePicker(isPresented: Bool) { + self.isPresentedPrizeAtDatePicker = isPresented + } + +} diff --git a/Projects/Feature/MyPageFeature/Sources/Model/MyPageProfileModel.swift b/Projects/Feature/MyPageFeature/Sources/Model/MyPageProfileModel.swift new file mode 100644 index 00000000..82a23177 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Model/MyPageProfileModel.swift @@ -0,0 +1,92 @@ +import Foundation + +protocol MyPageProfileStateProtocol { + var profileURL: String { get } + var introduce: String { get } + var email: String { get } + var major: String { get } + var majorList: [String] { get } + var portfolioURL: String { get } + var techStacks: [String] { get } + var isSelfEntering: Bool { get } + var isPresentedMajorSheet: Bool { get } + var isPresentedImageMethodPicker: Bool { get } + var isPresentedProfileCamera: Bool { get } + var isPresentedProfileImage: Bool { get } + var isPresentedTechStackAppend: Bool { get } +} + +protocol MyPageProfileActionProtocol: AnyObject { + func updateProfileURL(url: String) + func updateIntroduce(introduce: String) + func updateEmail(email: String) + func updateMajor(major: String) + func updateMajorList(majorList: [String]) + func updatePortfolioURL(portfolioURL: String) + func updateTechStacks(techStacks: [String]) + func removeTechStack(techStack: String) + func updateIsSelfEntering(isSelfEntering: Bool) + func updateIsPresentedMajorSheet(isPresented: Bool) + func updateIsPresentedImageMethodPicker(isPresented: Bool) + func updateIsPresentedProfileCamera(isPresented: Bool) + func updateIsPresentedProfileImage(isPresented: Bool) + func updateIsPresentedTechStackAppend(isPresented: Bool) +} + +extension MyPageModel: MyPageProfileActionProtocol { + func updateProfileURL(url: String) { + self.profileURL = url + } + + func updateIntroduce(introduce: String) { + self.introduce = introduce + } + + func updateEmail(email: String) { + self.email = email + } + + func updateMajor(major: String) { + self.major = major + } + + func updateMajorList(majorList: [String]) { + self.majorList = majorList + } + + func updatePortfolioURL(portfolioURL: String) { + self.portfolioURL = portfolioURL + } + + func updateTechStacks(techStacks: [String]) { + self.techStacks = techStacks + } + + func removeTechStack(techStack: String) { + self.techStacks.removeAll { $0 == techStack } + } + + func updateIsSelfEntering(isSelfEntering: Bool) { + self.isSelfEntering = isSelfEntering + } + + func updateIsPresentedMajorSheet(isPresented: Bool) { + self.isPresentedMajorSheet = isPresented + } + + func updateIsPresentedImageMethodPicker(isPresented: Bool) { + self.isPresentedImageMethodPicker = isPresented + } + + func updateIsPresentedProfileCamera(isPresented: Bool) { + self.isPresentedProfileCamera = isPresented + } + + func updateIsPresentedProfileImage(isPresented: Bool) { + self.isPresentedProfileImage = isPresented + } + + func updateIsPresentedTechStackAppend(isPresented: Bool) { + self.isPresentedTechStackAppend = isPresented + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Model/MyPageProjectModel.swift b/Projects/Feature/MyPageFeature/Sources/Model/MyPageProjectModel.swift new file mode 100644 index 00000000..1674a158 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Model/MyPageProjectModel.swift @@ -0,0 +1,213 @@ +import DateUtil +import Foundation +import FoundationUtil + +struct ProjectModel { + var name: String + var iconImage: String + var previewImages: [String] + var content: String + var techStacks: Set + var mainTask: String + var startAt: Date + var endAt: Date? + var isInProgress: Bool + var relatedLinks: [RelatedLink] + + var startAtString: String { + startAt.toStringCustomFormat(format: "yyyy.MM") + } + var endAtString: String { + endAt?.toStringCustomFormat(format: "yyyy.MM") ?? "" + } +} + +extension ProjectModel { + struct RelatedLink { + var name: String + var url: String + } +} + +protocol MyPageProjectStateProtocol { + var projectList: [ProjectModel] { get } + var collapsedProject: [Bool] { get } + var focusedProjectIndex: Int { get } + var isPresentedProjectImagePicker: Bool { get } + var isPresentedPreviewImagePicker: Bool { get } + var isPresentedProjectStartAtDatePicker: Bool { get } + var isPresentedProjectEndAtDatePicker: Bool { get } + var isPresentedProjectTechStackAppend: Bool { get } + var isPresentedProjectToast: Bool { get } +} + +protocol MyPageProjectActionProtocol: AnyObject { + func toggleCollapsedProject(index: Int) + func updateProjectList(projectList: [ProjectModel]) + func updateProjectName(index: Int, name: String) + func updateIconImage(index: Int, imageURL: String) + func appendPreviewImage(index: Int, imageURL: String) + func removePreviewImage(index: Int, previewIndex: Int) + func updateProjectContent(index: Int, content: String) + func updateProjectTechStacks(index: Int, techStacks: [String]) + func removeProjectTechStack(index: Int, techStack: String) + func updateProjectMainTask(index: Int, mainTask: String) + func updateProjectStartAt(index: Int, startAt: Date) + func updateProjectEndAt(index: Int, endAt: Date) + func updateIsInProgress(index: Int, isInProgress: Bool) + func updateProjectLinkName(index: Int, linkIndex: Int, name: String) + func updateProjectLinkURL(index: Int, linkIndex: Int, url: String) + func appendEmptyRelatedLink(index: Int) + func removeProjectRelatedLink(index: Int, linkIndex: Int) + func appendEmptyProject() + func removeProject(index: Int) + func updateFocusedProjectIndex(index: Int) + func updateIsPresentedProjectImagePicker(isPresented: Bool) + func updateIsPresentedPreviewImagePicker(isPresented: Bool) + func updateIsPresentedProjectStartAtDatePicker(isPresented: Bool) + func updateIsPresentedProjectEndAtDatePicker(isPresented: Bool) + func updateIsPresentedProjectTechStackAppend(isPresented: Bool) + func updateIsPresentedProjectToast(isPresented: Bool) +} + +extension MyPageModel: MyPageProjectActionProtocol { + func toggleCollapsedProject(index: Int) { + guard collapsedProject[safe: index] != nil else { return } + collapsedProject[index].toggle() + } + + func updateProjectList(projectList: [ProjectModel]) { + self.projectList = projectList + } + + func updateProjectName(index: Int, name: String) { + guard projectList[safe: index] != nil else { return } + projectList[index].name = name + } + + func updateIconImage(index: Int, imageURL: String) { + guard projectList[safe: index] != nil else { return } + projectList[index].iconImage = imageURL + } + + func appendPreviewImage(index: Int, imageURL: String) { + guard projectList[safe: index] != nil else { return } + projectList[index].previewImages.append(imageURL) + } + + func removePreviewImage(index: Int, previewIndex: Int) { + guard projectList[safe: index] != nil else { return } + guard projectList[index].previewImages[safe: previewIndex] != nil else { return } + projectList[index].previewImages.remove(at: previewIndex) + } + + func updateProjectContent(index: Int, content: String) { + guard projectList[safe: index] != nil else { return } + projectList[index].content = content + } + + func updateProjectTechStacks(index: Int, techStacks: [String]) { + guard projectList[safe: index] != nil else { return } + projectList[index].techStacks = Set(techStacks) + } + + func removeProjectTechStack(index: Int, techStack: String) { + guard projectList[safe: index] != nil else { return } + projectList[index].techStacks.remove(techStack) + } + + func updateProjectMainTask(index: Int, mainTask: String) { + guard projectList[safe: index] != nil else { return } + projectList[index].mainTask = mainTask + } + + func updateProjectStartAt(index: Int, startAt: Date) { + guard projectList[safe: index] != nil else { return } + projectList[index].startAt = startAt + } + + func updateProjectEndAt(index: Int, endAt: Date) { + guard projectList[safe: index] != nil else { return } + projectList[index].endAt = endAt + } + + func updateIsInProgress(index: Int, isInProgress: Bool) { + guard projectList[safe: index] != nil else { return } + projectList[index].isInProgress = isInProgress + } + + func updateProjectLinkName(index: Int, linkIndex: Int, name: String) { + guard projectList[safe: index] != nil else { return } + guard projectList[index].relatedLinks[safe: linkIndex] != nil else { return } + projectList[index].relatedLinks[linkIndex].name = name + } + + func updateProjectLinkURL(index: Int, linkIndex: Int, url: String) { + guard projectList[safe: index] != nil else { return } + guard projectList[index].relatedLinks[safe: linkIndex] != nil else { return } + projectList[index].relatedLinks[linkIndex].url = url + } + + func appendEmptyRelatedLink(index: Int) { + guard projectList[safe: index] != nil else { return } + projectList[index].relatedLinks.append(.init(name: "", url: "")) + } + + func removeProjectRelatedLink(index: Int, linkIndex: Int) { + guard projectList[safe: index] != nil else { return } + guard projectList[index].relatedLinks[safe: linkIndex] != nil else { return } + projectList[index].relatedLinks.remove(at: linkIndex) + } + + func appendEmptyProject() { + projectList.append( + .init( + name: "", + iconImage: "", + previewImages: [], + content: "", + techStacks: [], + mainTask: "", + startAt: Date(), + endAt: nil, + isInProgress: false, + relatedLinks: [] + ) + ) + collapsedProject.append(false) + } + + func removeProject(index: Int) { + guard projectList[safe: index] != nil, collapsedProject[safe: index] != nil else { return } + projectList.remove(at: index) + collapsedProject.remove(at: index) + } + + func updateFocusedProjectIndex(index: Int) { + focusedProjectIndex = index + } + + func updateIsPresentedProjectImagePicker(isPresented: Bool) { + isPresentedProjectImagePicker = isPresented + } + + func updateIsPresentedPreviewImagePicker(isPresented: Bool) { + isPresentedPreviewImagePicker = isPresented + } + + func updateIsPresentedProjectStartAtDatePicker(isPresented: Bool) { + isPresentedProjectStartAtDatePicker = isPresented + } + + func updateIsPresentedProjectEndAtDatePicker(isPresented: Bool) { + isPresentedProjectEndAtDatePicker = isPresented + } + + func updateIsPresentedProjectTechStackAppend(isPresented: Bool) { + isPresentedProjectTechStackAppend = isPresented + } + + func updateIsPresentedProjectToast(isPresented: Bool) { + isPresentedProjectToast = isPresented + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Model/MyPageSchoolLifeModel.swift b/Projects/Feature/MyPageFeature/Sources/Model/MyPageSchoolLifeModel.swift new file mode 100644 index 00000000..48392fc0 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Model/MyPageSchoolLifeModel.swift @@ -0,0 +1,13 @@ +protocol MyPageSchoolLifeStateProtocol { + var gsmScore: String { get } +} + +protocol MyPageSchoolLifeActionProtocol: AnyObject { + func updateGSMScore(gsmScore: String) +} + +extension MyPageModel: MyPageSchoolLifeActionProtocol { + func updateGSMScore(gsmScore: String) { + self.gsmScore = gsmScore + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Model/MyPageWorkInfoModel.swift b/Projects/Feature/MyPageFeature/Sources/Model/MyPageWorkInfoModel.swift new file mode 100644 index 00000000..bf02421f --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Model/MyPageWorkInfoModel.swift @@ -0,0 +1,51 @@ +import FoundationUtil +import StudentDomainInterface + +protocol MyPageWorkInfoStateProtocol { + var workRegionList: [String] { get } + var salary: String { get } + var formOfEmployment: FormOfEmployment { get } + var isPresentedFormOfEmployeementSheet: Bool { get } +} + +protocol MyPageWorkInfoActionProtocol: AnyObject { + func appendWorkRegion() + func updateWorkRegion(region: String, at index: Int) + func updateWorkRegions(regions: [String]) + func deleteWorkRegion(at index: Int) + func updateSalary(salary: String) + func updateFormOfEmployment(form: FormOfEmployment) + func updateIsPresentedFormOfEmployeementSheet(isPresented: Bool) +} + +extension MyPageModel: MyPageWorkInfoActionProtocol { + func appendWorkRegion() { + self.workRegionList.append("") + } + + func updateWorkRegion(region: String, at index: Int) { + guard workRegionList[safe: index] != nil else { return } + self.workRegionList[index] = region + } + + func updateWorkRegions(regions: [String]) { + self.workRegionList = regions + } + + func deleteWorkRegion(at index: Int) { + self.workRegionList.remove(at: index) + } + + func updateSalary(salary: String) { + guard let salaryInt = Int(salary).map({ String(min($0, 9999)) }) else { return } + self.salary = salaryInt == "0" ? "상관없음" : "\(salaryInt)만원" + } + + func updateFormOfEmployment(form: FormOfEmployment) { + self.formOfEmployment = form + } + + func updateIsPresentedFormOfEmployeementSheet(isPresented: Bool) { + self.isPresentedFormOfEmployeementSheet = isPresented + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Scene/MyPageCertificateView.swift b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageCertificateView.swift new file mode 100644 index 00000000..a65e44c2 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageCertificateView.swift @@ -0,0 +1,56 @@ +import BaseFeature +import DesignSystem +import SwiftUI +import ViewUtil + +struct MyPageCertificateView: View { + @StateObject var container: MVIContainer + var intent: MyPageCertificateIntentProtocol { container.intent } + var state: MyPageCertificateStateProtocol { container.model } + + init(container: MVIContainer) { + self._container = StateObject(wrappedValue: container) + } + + var body: some View { + Section { + VStack(spacing: 8) { + certificateListView() + .titleWrapper("자격증") + .aligned(.leading) + + SMSChip("추가") { + intent.certificateAppendButtonDidTap() + } + .aligned(.leading) + } + .animation(.default, value: state.certificates.count) + } header: { + SectionHeaderView(title: "자격증") + } + .padding(.horizontal, 20) + } + + @ViewBuilder + func certificateListView() -> some View { + VStack(spacing: 12) { + ForEach(state.certificates.indices, id: \.self) { index in + HStack(spacing: 16) { + SMSTextField( + "정보처리산업기사", + text: Binding( + get: { state.certificates[safe: index] ?? "" }, + set: { intent.updateCertificate(certificate: $0, at: index) } + ) + ) + + Button { + intent.deleteCertificateColumn(at: index) + } label: { + SMSIcon(.trash) + } + } + } + } + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Scene/MyPageLanguageView.swift b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageLanguageView.swift new file mode 100644 index 00000000..3b9dab48 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageLanguageView.swift @@ -0,0 +1,69 @@ +import BaseFeature +import DesignSystem +import SwiftUI +import ViewUtil + +struct MyPageLanguageView: View { + @StateObject var container: MVIContainer + var intent: MyPageLanguageInfoIntentProtocol { container.intent } + var state: MyPageLanguageInfoStateProtocol { container.model } + let geometry: GeometryProxy + + init( + container: MVIContainer, + geometry: GeometryProxy + ) { + self._container = StateObject(wrappedValue: container) + self.geometry = geometry + } + + var body: some View { + Section { + languageListView(proxy: geometry) + } header: { + SectionHeaderView(title: "외국어") + } + .padding(.horizontal, 20) + } + + @ViewBuilder + func languageListView(proxy: GeometryProxy) -> some View { + VStack(spacing: 8) { + ForEach(state.languageList.indices, id: \.self) { index in + HStack(spacing: 16) { + SMSTextField( + "예) 영어, 토익", + text: Binding( + get: { state.languageList[safe: index]?.name ?? "" }, + set: { intent.updateLanguageName(name: $0, at: index) } + ) + ) + .frame(maxWidth: .infinity) + + SMSTextField( + "원어민수준", + text: Binding( + get: { state.languageList[safe: index]?.score ?? "" }, + set: { intent.updateLanguageScore(score: $0, at: index) } + ) + ) + .frame(maxWidth: proxy.size.width / 4) + + Button { + intent.deleteLanguage(at: index) + } label: { + SMSIcon(.trash) + } + } + } + .titleWrapper("외국어") + .aligned(.leading) + + SMSChip("추가") { + intent.languageAppendButtonDidTap() + } + .aligned(.leading) + } + .animation(.default, value: state.languageList.count) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Scene/MyPageMilitaryView.swift b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageMilitaryView.swift new file mode 100644 index 00000000..cac51c3e --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageMilitaryView.swift @@ -0,0 +1,43 @@ +import DesignSystem +import SwiftUI +import ViewUtil +import BaseFeature + +struct MyPageMilitaryView: View { + @StateObject var container: MVIContainer + var intent: MyPageMilitaryIntentProtocol { container.intent } + var state: MyPageMilitaryStateProtocol { container.model } + + init( + container: MVIContainer + ) { + self._container = StateObject(wrappedValue: container) + } + + var body: some View { + Section { + VStack(spacing: 24) { + SMSTextField( + "병특 희망", + text: Binding( + get: { state.selectedMilitaryServiceType.display() }, + set: { _ in } + ), + isOnClear: false + ) + .disabled(true) + .overlay(alignment: .trailing) { + SMSIcon(.downChevron) + .padding(.trailing, 12) + } + .titleWrapper("병특 희망 여부") + .onTapGesture { + intent.militarySheetIsRequired() + } + } + } header: { + SectionHeaderView(title: "병역") + } + .padding(.horizontal, 20) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Scene/MyPagePrizeView.swift b/Projects/Feature/MyPageFeature/Sources/Scene/MyPagePrizeView.swift new file mode 100644 index 00000000..a0417bad --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Scene/MyPagePrizeView.swift @@ -0,0 +1,113 @@ +import BaseFeature +import DesignSystem +import SwiftUI +import ViewUtil + +struct MyPagePrizeView: View { + @StateObject var container: MVIContainer + var intent: MyPagePrizeIntentProtocol { container.intent } + var state: MyPagePrizeStateProtocol { container.model } + + init(container: MVIContainer) { + self._container = StateObject(wrappedValue: container) + } + + var body: some View { + Section { + VStack(spacing: 24) { + ForEach(state.prizeList.indices, id: \.self) { index in + prizeListRowView(index: index) + + SMSSeparator(height: 1) + } + } + + Spacer().frame(height: 16) + + HStack(spacing: 4) { + SMSIcon(.plus, width: 12, height: 12) + .foregroundColor(.sms(.system(.black))) + + SMSText("추가") + .foregroundColor(.sms(.system(.black))) + .font(.sms(.title2)) + } + .aligned(.trailing) + .buttonWrapper { + intent.prizeAppendButtonDidTap() + } + + Spacer().frame(height: 32) + } header: { + SectionHeaderView(title: "수상") + } + .padding(.horizontal, 20) + } + + @ViewBuilder + func prizeListRowView(index: Int) -> some View { + let collapsed = state.collapsedPrize[safe: index] ?? false + Section { + ConditionView(!collapsed) { + prizeName(index: index) + + prizePrize(index: index) + + prizePrizeAt(index: index) + } + } header: { + HStack(spacing: 16) { + let prizeName = state.prizeList[safe: index]?.name ?? "" + SMSText(prizeName.isEmpty ? "수상" : prizeName, font: .title1) + .foregroundColor(.sms(.system(.black))) + + Spacer() + + SMSIcon(.downChevron) + .rotationEffect(collapsed ? .degrees(90) : .degrees(0)) + .buttonWrapper { + intent.prizeToggleButtonDidTap(index: index) + } + + SMSIcon(.xmarkOutline) + .buttonWrapper { + intent.prizeRemoveButtonDidTap(index: index) + } + } + .padding(.bottom, 8) + } + } + + @ViewBuilder + func prizeName(index: Int) -> some View { + SMSTextField( + "수상 내역 이름입력", + text: Binding( + get: { state.prizeList[safe: index]?.name ?? "" }, + set: { intent.updatePrizeName(index: index, name: $0) } + ) + ) + .titleWrapper("이름") + } + + @ViewBuilder + func prizePrize(index: Int) -> some View { + SMSTextField( + "수상 종류입력", + text: Binding( + get: { state.prizeList[safe: index]?.prize ?? "" }, + set: { intent.updatePrizePrize(index: index, prize: $0)} + ) + ) + .titleWrapper("종류") + } + + @ViewBuilder + func prizePrizeAt(index: Int) -> some View { + let prize = state.prizeList[safe: index] + DatePickerField(dateText: prize?.prizeAtString ?? "") { + intent.prizeAtButtonDidTap(index: index) + } + .titleWrapper("수상 일자") + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Scene/MyPageProfileView.swift b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageProfileView.swift new file mode 100644 index 00000000..73fe9f59 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageProfileView.swift @@ -0,0 +1,154 @@ +import DesignSystem +import NukeUI +import SwiftUI +import TagLayoutView +import ViewUtil + +struct MyPageProfileView: View { + let intent: MyPageProfileIntentProtocol + let state: MyPageProfileStateProtocol + let geometry: GeometryProxy + + init( + intent: MyPageProfileIntentProtocol, + state: MyPageProfileStateProtocol, + geometry: GeometryProxy + ) { + self.intent = intent + self.state = state + self.geometry = geometry + } + + var body: some View { + Section { + VStack(alignment: .leading, spacing: 24) { + VStack(alignment: .leading, spacing: 8) { + ZStack(alignment: .bottomTrailing) { + if let profileImageURL = URL(string: state.profileURL) { + LazyImage(url: profileImageURL) { image in + if let image = image.image { + image.resizable() + .frame(width: 100, height: 100) + .cornerRadius(8) + } else { + Color.sms(.neutral(.n30)) + .frame(width: 100, height: 100) + } + } + } else { + SMSIcon(.profile, width: 100, height: 100) + } + + SMSIcon(.profileSmallPlus) + .overlay { + RoundedRectangle(cornerRadius: 7) + .strokeBorder(Color.sms(.system(.white)), lineWidth: 4) + } + .offset(x: 5, y: 4) + } + .buttonWrapper { + withAnimation { + intent.imageMethodPickerIsRequired() + } + } + .titleWrapper("사진") + } + + SMSTextField( + "1줄 자기소개 입력", + text: Binding(get: { state.introduce }, set: intent.updateIntroduce(introduce:)), + errorText: "1글자에서 50글자 사이로 입력해주세요" + ) + .titleWrapper("자기소개") + + SMSTextField( + "공개용 이메일 입력", + text: Binding(get: { state.email }, set: intent.updateEmail(email:)), + errorText: "이메일 형식에 맞게 입력해주세요" + ) + .keyboardType(.emailAddress) + .titleWrapper("이메일") + + SMSTextField( + state.isSelfEntering ? "전공 분야 입력" : "전공 분야 선택", + text: Binding(get: { state.major }, set: intent.updateMajor(major:)), + errorText: "전공 분야를 선택해주세요", + isOnClear: false + ) + .disabled(!state.isSelfEntering) + .overlay(alignment: .topTrailing) { + SMSIcon(.downChevron) + .padding([.top, .trailing], 12) + } + .simultaneousGesture( + TapGesture() + .onEnded { + intent.majorSheetIsRequired() + intent.deActiveSelfEntering() + } + ) + .titleWrapper("분야") + + SMSTextField( + "E.g. https://github.com", + text: Binding( + get: { state.portfolioURL }, + set: intent.updatePortfolioURL(portfolioURL:) + ), + errorText: "URL 형식에 맞게 입력해주세요" + ) { + intent.techStackAppendIsRequired() + } + .keyboardType(.URL) + .titleWrapper("포트폴리오 URL") + + VStack(spacing: 8) { + HStack(spacing: 8) { + SMSIcon(.magnifyingglass) + + SMSText("찾고 싶은 세부 스택 입력", font: .body1) + .foregroundColor(.sms(.neutral(.n30))) + + Spacer() + } + .padding(12) + .background { + Color.sms(.neutral(.n10)) + } + .clipShape(RoundedRectangle(cornerRadius: 8)) + .buttonWrapper { + intent.techStackAppendIsRequired() + } + + TagLayoutView( + state.techStacks, + tagFont: UIFont( + font: DesignSystemFontFamily.Pretendard.regular, + size: 24 + ) ?? .init(), + padding: 20, + parentWidth: geometry.size.width + ) { techStack in + HStack { + SMSText(techStack, font: .body2) + + SMSIcon(.xmarkOutline, width: 20, height: 20) + .buttonWrapper { + intent.removeTechStack(techStack: techStack) + } + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + .background(Color.sms(.neutral(.n10))) + .fixedSize() + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + } + .titleWrapper("세부스택 (최대 5개)") + } + } header: { + SectionHeaderView(title: "프로필") + } + .padding(.horizontal, 20) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Scene/MyPageProjectView.swift b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageProjectView.swift new file mode 100644 index 00000000..6941f861 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageProjectView.swift @@ -0,0 +1,370 @@ +import BaseFeature +import DesignSystem +import NukeUI +import SwiftUI +import TagLayoutView +import ViewUtil + +struct MyPageProjectView: View { + @FocusState var projectContentIsFocused: Bool + @StateObject var container: MVIContainer + var intent: MyPageProjectIntentProtocol { container.intent } + var state: MyPageProjectStateProtocol { container.model } + let geometry: GeometryProxy + + init( + container: MVIContainer, + geometry: GeometryProxy + ) { + self._container = StateObject(wrappedValue: container) + self.geometry = geometry + } + + var body: some View { + Section { + VStack(spacing: 16) { + VStack(spacing: 24) { + ForEach(state.projectList.indices, id: \.self) { index in + projectListRowView(index: index, geometry: geometry) + + SMSSeparator(height: 1) + } + } + + HStack(spacing: 4) { + SMSIcon(.plus, width: 12, height: 12) + .foregroundColor(.sms(.system(.black))) + + SMSText("추가") + .foregroundColor(.sms(.system(.black))) + .font(.sms(.title2)) + } + .aligned(.trailing) + .buttonWrapper { + intent.projectAppendButtonDidTap() + } + } + } header: { + SectionHeaderView(title: "프로젝트") + } + .padding(.horizontal, 20) + } + + @ViewBuilder + func projectListRowView(index: Int, geometry: GeometryProxy) -> some View { + let collapsed = state.collapsedProject[safe: index] ?? false + Section { + VStack(alignment: .leading, spacing: 24) { + ConditionView(!collapsed) { + projectName(index: index) + + projectIcon(index: index) + + projectPreviewImageList(index: index) + + projectContentTextEditor(index: index) + + projectTechStack(geometry: geometry, index: index) + + projectDuration(index: index) + + projectRelatedLink(index: index, geometry: geometry) + } + } + } header: { + HStack(spacing: 16) { + let projectName = state.projectList[safe: index]?.name ?? "" + SMSText(projectName.isEmpty ? "프로젝트" : projectName, font: .title1) + .foregroundColor(.sms(.system(.black))) + + Spacer() + + SMSIcon(.downChevron) + .rotationEffect(collapsed ? .degrees(90) : .degrees(0)) + .buttonWrapper { + intent.projectToggleButtonDidTap(index: index) + } + + SMSIcon(.xmarkOutline) + .buttonWrapper { + intent.projectRemoveButtonDidTap(index: index) + } + } + .padding(.bottom, 8) + } + } +} + +// MARK: - View Section +private extension MyPageProjectView { + @ViewBuilder + func projectName(index: Int) -> some View { + SMSTextField( + "프로젝트 이름 입력", + text: Binding( + get: { state.projectList[safe: index]?.name ?? "" }, + set: { intent.updateProjectName(index: index, name: $0) } + ) + ) + .titleWrapper("이름") + } + + @ViewBuilder + func projectIcon(index: Int) -> some View { + Group { + if let iconURLString = state.projectList[safe: index]?.iconImage, + let iconURL = URL(string: iconURLString) { + LazyImage(url: iconURL) { image in + if let image = image.image { + image + .resizable() + .frame(width: 108, height: 108) + .cornerRadius(8) + } else { + imagePlaceholder(size: 108) + .overlay { + SMSIcon(.photo) + } + } + } + } else { + imagePlaceholder(size: 108) + .overlay { + SMSIcon(.photo) + } + } + } + .buttonWrapper { + intent.projectIconImageButtonDidTap(index: index) + } + .titleWrapper("아이콘") + .imagePicker( + isShow: Binding( + get: { state.isPresentedProjectImagePicker && state.focusedProjectIndex == index }, + set: { _ in intent.projectIconImagePickerDismissed() } + ), + pickedImageResult: Binding( + get: { nil }, + set: { + guard let image = $0 else { return } + intent.updateIconImage(index: index, image: image) + } + ) + ) + } + + @ViewBuilder + func projectPreviewImageList(index: Int) -> some View { + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 8) { + let projectPreviewImages = state.projectList[safe: index]?.previewImages ?? [] + imagePlaceholder(size: 132) + .overlay { + VStack(spacing: 4) { + SMSIcon(.photo) + SMSText( + "\(projectPreviewImages.count)/4", + font: .body2 + ) + .foregroundColor(.sms(.system(.black))) + } + } + .buttonWrapper { + intent.appendPreviewImageButtonDidTap(index: index, previewsCount: projectPreviewImages.count) + } + + ForEach(projectPreviewImages.indices, id: \.self) { previewIndex in + let url = URL(string: projectPreviewImages[previewIndex]) + LazyImage(url: url) { image in + if let image = image.image { + image + .resizable() + .frame(width: 132, height: 132) + .cornerRadius(8) + .overlay(alignment: .topTrailing) { + SMSIcon(.xmark) + .padding(4) + .buttonWrapper { + intent.removePreviewImageDidTap(index: index, previewIndex: previewIndex) + } + } + } else { + imagePlaceholder(size: 132) + } + } + } + } + } + .titleWrapper("미리보기 사진") + } + + @ViewBuilder + func projectContentTextEditor(index: Int) -> some View { + let projectContent = state.projectList[safe: index]?.content ?? "" + TextEditor( + text: Binding( + get: { projectContent }, + set: { intent.updateProjectContent(index: index, content: $0) } + ) + ) + .smsFont(.body1, color: .system(.black)) + .focused($projectContentIsFocused) + .colorMultiply(.sms(.neutral(.n10))) + .frame(minHeight: 48) + .cornerRadius(8) + .roundedStroke( + cornerRadius: 8, + color: projectContentIsFocused ? .sms(.primary(.p1)) : .clear, + lineWidth: projectContentIsFocused ? 1 : 0 + ) + .overlay(alignment: .topLeading) { + ConditionView(projectContent.isEmpty) { + SMSText("프로젝트 내용 입력", font: .body1) + .foregroundColor(.sms(.neutral(.n30))) + .padding([.top, .leading], 12) + .onTapGesture { + projectContentIsFocused = true + } + } + } + .titleWrapper("내용") + } + + @ViewBuilder + func projectTechStack(geometry: GeometryProxy, index: Int) -> some View { + VStack(spacing: 8) { + HStack(spacing: 8) { + SMSIcon(.magnifyingglass) + + SMSText("찾고 싶은 세부 스택 입력", font: .body1) + .foregroundColor(.sms(.neutral(.n30))) + + Spacer() + } + .padding(12) + .background { + Color.sms(.neutral(.n10)) + } + .clipShape(RoundedRectangle(cornerRadius: 8)) + .buttonWrapper { + intent.projectTechStackAppendButtonDidTap(index: index) + } + + TagLayoutView( + Array(state.projectList[safe: index]?.techStacks ?? []), + tagFont: UIFont( + font: DesignSystemFontFamily.Pretendard.regular, + size: 24 + ) ?? .init(), + padding: 20, + parentWidth: geometry.size.width + ) { techStack in + HStack { + SMSText(techStack, font: .body2) + + SMSIcon(.xmarkOutline, width: 20, height: 20) + .buttonWrapper { + intent.removeProjectTechStackButtonDidTap(index: index, techStack: techStack) + } + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + .background(Color.sms(.neutral(.n10))) + .fixedSize() + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + } + .titleWrapper("사용 기술 (최대 20개)") + } + + @ViewBuilder + func projectDuration(index: Int) -> some View { + VStack(spacing: 8) { + HStack(spacing: 8) { + let project = state.projectList[safe: index] + DatePickerField(dateText: project?.startAtString ?? "") { + intent.projectStartAtButtonDidTap(index: index) + } + .frame(maxWidth: .infinity) + + if !(project?.isInProgress ?? false) { + SMSIcon(.waterWave) + + DatePickerField(dateText: project?.endAtString ?? "") { + intent.projectEndAtButtonDidTap(index: index) + } + .frame(maxWidth: .infinity) + } + } + .animation(.spring(blendDuration: 0.3), value: state.projectList.map(\.isInProgress)) + + HStack(spacing: 8) { + SMSCheckbox( + isSelected: Binding( + get: { state.projectList[safe: index]?.isInProgress ?? false }, + set: { isInProgress in + withAnimation { + intent.projectIsInProgressButtonDidTap(index: index, isInProgress: isInProgress) + } + } + ) + ) + + SMSText("진행중", font: .body1) + .foregroundColor(.sms(.neutral(.n30))) + .aligned(.leading) + } + } + .titleWrapper("진행 기간") + } + + @ViewBuilder + func projectRelatedLink(index: Int, geometry: GeometryProxy) -> some View { + VStack(spacing: 8) { + let relatedLinks = state.projectList[safe: index]?.relatedLinks ?? [] + ForEach(relatedLinks.indices, id: \.self) { relatedIndex in + HStack(spacing: 16) { + SMSTextField( + "이름", + text: Binding( + get: { relatedLinks[relatedIndex].name }, + set: { intent.updateProjectLinkName(index: index, linkIndex: relatedIndex, name: $0) } + ) + ) + .frame(maxWidth: geometry.size.width / 4) + + SMSTextField( + "URL", + text: Binding( + get: { relatedLinks[relatedIndex].url }, + set: { intent.updateProjectLinkURL(index: index, linkIndex: relatedIndex, url: $0) } + ) + ) + .frame(maxWidth: .infinity) + + Button { + intent.removeProjectRelatedLinkDidTap(index: index, linkIndex: relatedIndex) + } label: { + SMSIcon(.trash) + } + } + } + + SMSChip("추가") { + intent.relatedLinkAppendButtonDidTap(index: index) + } + .aligned(.leading) + } + .titleWrapper("관련 링크") + } +} + +// MARK: - Reusable +private extension MyPageProjectView { + @ViewBuilder + func imagePlaceholder(size: CGFloat) -> some View { + RoundedRectangle(cornerRadius: 8) + .fill(Color.sms(.neutral(.n10))) + .frame(width: size, height: size) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Scene/MyPageSchoolLifeView.swift b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageSchoolLifeView.swift new file mode 100644 index 00000000..05b2eae9 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageSchoolLifeView.swift @@ -0,0 +1,27 @@ +import DesignSystem +import SwiftUI + +struct MyPageSchoolLifeView: View { + let intent: MyPageSchoolLifeIntentProtocol + let state: MyPageSchoolLifeStateProtocol + + var body: some View { + Section { + VStack(spacing: 24) { + SMSTextField( + "인증제 점수 입력", + text: Binding( + get: { state.gsmScore }, + set: intent.updateGSMScore(gsmScore:) + ), + errorText: "인증제 점수를 입력해주세요" + ) + .keyboardType(.numberPad) + .titleWrapper("인증제 점수") + } + } header: { + SectionHeaderView(title: "학교 생활") + } + .padding(.horizontal, 20) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Scene/MyPageView.swift b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageView.swift new file mode 100644 index 00000000..542de139 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageView.swift @@ -0,0 +1,438 @@ +import BaseFeature +import SwiftUI +import DesignSystem +import ViewUtil +import TechStackAppendFeatureInterface +import StudentDomainInterface + +struct MyPageView: View { + @Environment(\.dismiss) var dismiss + @Environment(\.safeAreaInsets) var safeAreaInsets + @StateObject var container: MVIContainer + var intent: any MyPageIntentProtocol { container.intent } + var state: any MyPageStateProtocol { container.model } + private let techStackAppendBuildable: any TechStackAppendBuildable + + init( + container: MVIContainer, + techStackAppendBuildable: any TechStackAppendBuildable + ) { + self.techStackAppendBuildable = techStackAppendBuildable + self._container = StateObject(wrappedValue: container) + } + + var body: some View { + GeometryReader { geometry in + VStack { + Spacer() + .frame(height: 1) + + myPageView(geometry: geometry) + + CTAButton(text: "저장") { + intent.modifyToInputAllInfo(state: state) + } + .padding(.horizontal, 20) + .padding(.bottom, safeAreaInsets.bottom + 16) + .background { + Color.sms(.system(.white)) + } + .ignoresSafeArea() + } + .onAppear { + intent.onAppear() + } + .hideKeyboardWhenTap() + } + .edgesIgnoringSafeArea([.bottom]) + .smsBottomSheet( + isShowing: Binding( + get: { state.isPresentedExitBottomSheet }, + set: { _ in intent.exitActionSheetDismissed() } + ) + ) { + VStack(alignment: .leading, spacing: 32) { + Button { + intent.logoutDialogIsRequired() + intent.exitActionSheetDismissed() + } label: { + HStack(spacing: 12) { + SMSIcon(.logout) + + SMSText("로그아웃", font: .title2) + .foregroundStyle(Color.sms(.neutral(.n50))) + + Spacer() + } + } + + Button { + intent.withdrawalDialogIsRequired() + intent.exitActionSheetDismissed() + } label: { + HStack(spacing: 12) { + SMSIcon(.redPerson) + + SMSText("회원탈퇴", font: .title2) + .foregroundStyle(Color.sms(.error(.e2))) + + Spacer() + } + } + } + .padding(.top, 12) + .padding(.horizontal, 20) + } + .animation(.default, value: state.isPresentedExitBottomSheet) + .smsBottomSheet( + isShowing: Binding( + get: { state.isPresentedMilitarySheet }, + set: { _ in intent.militarySheetDismissed() } + ) + ) { + militaryListView() + } + .animation(.default, value: state.isPresentedMilitarySheet) + .smsBottomSheet( + isShowing: Binding( + get: { state.isPresentedFormOfEmployeementSheet }, + set: { _ in intent.formOfEmployeementSheetDismissed() } + ) + ) { + DeferView { + formOfEmployeementList() + } + } + .animation(.default, value: state.isPresentedFormOfEmployeementSheet) + .smsToast( + text: "이미지는 최대 4개까지만 추가 할 수 있어요.", + isShowing: Binding( + get: { state.isPresentedProjectToast }, + set: { _ in intent.projectToastDismissed() } + ) + ) + .smsAlert( + title: "로그아웃", + description: "정말로 로그아웃 하시겠습니까?", + isShowing: Binding( + get: { state.isPresentedLogoutDialog }, + set: { _ in intent.logoutDialogDismissed() } + ), + alertActions: [ + .init(text: "취소", style: .outline) { + intent.logoutDialogDismissed() + }, + .init(text: "확인", style: .error) { + intent.logoutDialogIsComplete() + } + ] + ) + .smsAlert( + title: "회원탈퇴", + description: "정말로 회원탈퇴 하시겠습니까?", + isShowing: Binding( + get: { state.isPresentedWithdrawalDialog }, + set: { _ in intent.withdrawalDialogDismissed() } + ), + alertActions: [ + .init(text: "취소", style: .outline) { + intent.withdrawalDialogDismissed() + }, + .init(text: "확인", style: .error) { + intent.withdrawalDialogIsComplete() + } + ] + ) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + SMSIcon(.logoutLine) + .onTapGesture { + intent.exitActionSheetIsRequired() + } + } + } + .datePicker( + isShowing: Binding( + get: { state.isPresentedPrizeAtDatePicker }, + set: { _ in intent.prizeAtDismissed() } + ) + ) { date in + intent.prizePrizeAtDidSelect(index: state.focusedPrizeIndex, prizeAt: date) + } + .datePicker( + isShowing: Binding( + get: { state.isPresentedProjectStartAtDatePicker }, + set: { _ in intent.projectStartAtDatePickerDismissed() } + ) + ) { date in + intent.projectStartAtDidSelect(index: state.focusedProjectIndex, startAt: date) + } + .datePicker( + isShowing: Binding( + get: { state.isPresentedProjectEndAtDatePicker }, + set: { _ in intent.projectEndAtDatePickerDismissed() } + ) + ) { date in + intent.projectEndAtDidSelect(index: state.focusedProjectIndex, endAt: date) + } + .smsBottomSheet( + isShowing: Binding( + get: { state.isPresentedImageMethodPicker }, + set: { _ in intent.imageMethodPickerDismissed() } + ) + ) { + ImageMethodPickerView { + intent.imagePickerIsRequired() + intent.imageMethodPickerDismissed() + } cameraAction: { + intent.cameraIsRequired() + intent.imageMethodPickerDismissed() + } + } + .smsBottomSheet( + isShowing: Binding( + get: { state.isPresentedMajorSheet }, + set: { _ in intent.majorSheetDismissed() } + ), + topPadding: 150 + ) { + majorListView() + } + .animation(.default, value: state.isPresentedImageMethodPicker) + .navigationTitle("마이페이지") + .smsBackButton( + dismiss: dismiss + ) + .navigationBarTitleDisplayMode(.inline) + } + + @ViewBuilder + func majorListView() -> some View { + ScrollView { + LazyVStack(spacing: 8) { + ForEach(state.majorList, id: \.self) { major in + MajorRowView( + text: major, + isSeleted: Binding( + get: { state.major == major }, + set: { + $0 ? intent.updateMajor(major: major) : () + $0 ? intent.majorSheetDismissed() : () + } + ) + ) + } + + MajorRowView( + text: "직접입력", + isSeleted: Binding( + get: { state.isSelfEntering }, + set: { + $0 ? intent.updateMajor(major: "") : () + $0 ? intent.activeSelfEntering() : () + $0 ? intent.majorSheetDismissed() : () + } + ) + ) + } + } + } + + @ViewBuilder + func formOfEmployeementList() -> some View { + VStack(spacing: 16) { + ForEach(FormOfEmployment.allCases, id: \.self) { formOfEmployment in + HStack { + Text(formOfEmployment.display()) + .smsFont(.body1, color: .neutral(.n50)) + + Spacer() + + SMSRadioButton( + isSelected: Binding( + get: { state.formOfEmployment == formOfEmployment }, + set: { $0 ? intent.updateFormOfEmployment(form: formOfEmployment) : () } + ) + ) + .buttonWrapper {} + } + .animation(.default, value: state.formOfEmployment) + .padding(.horizontal, 20) + } + } + } + + @ViewBuilder + func militaryListView() -> some View { + VStack(spacing: 16) { + ForEach(MilitaryServiceType.allCases, id: \.self) { militaryServiceType in + HStack { + Text(militaryServiceType.display()) + .smsFont(.body1, color: .neutral(.n50)) + + Spacer() + + SMSRadioButton( + isSelected: Binding( + get: { state.selectedMilitaryServiceType == militaryServiceType }, + set: { $0 ? intent.militaryServiceTypeDidSelected(type: militaryServiceType) : () } + ) + ) + .buttonWrapper {} + } + .animation(.default, value: state.selectedMilitaryServiceType) + .padding(.horizontal, 20) + } + } + } + + @ViewBuilder + func myPageView(geometry: GeometryProxy) -> some View { + ScrollView { + LazyVStack(pinnedViews: [.sectionHeaders]) { + Group { + SMSSeparator() + .padding(.vertical, 16) + + MyPageProfileView(intent: intent, state: state, geometry: geometry) + + SMSSeparator() + .padding(.vertical, 16) + + MyPageSchoolLifeView(intent: intent, state: state) + + SMSSeparator() + .padding(.vertical, 16) + + MyPageWorkInfoView( + container: .init( + intent: intent, + model: state, + modelChangePublisher: container.objectWillChange + ) + ) + + SMSSeparator() + .padding(.vertical, 16) + + MyPageMilitaryView( + container: .init( + intent: intent, + model: state, + modelChangePublisher: container.objectWillChange + ) + ) + } + + Group { + SMSSeparator() + .padding(.vertical, 16) + + MyPageCertificateView( + container: .init( + intent: intent, + model: state, + modelChangePublisher: container.objectWillChange + ) + ) + + SMSSeparator() + .padding(.vertical, 16) + + MyPageLanguageView( + container: .init( + intent: intent, + model: state, + modelChangePublisher: container.objectWillChange + ), + geometry: geometry + ) + + SMSSeparator() + .padding(.vertical, 16) + + MyPageProjectView( + container: .init( + intent: intent, + model: state, + modelChangePublisher: container.objectWillChange + ), + geometry: geometry + ) + + SMSSeparator() + .padding(.vertical, 16) + + MyPagePrizeView( + container: .init( + intent: intent, + model: state, + modelChangePublisher: container.objectWillChange + ) + ) + } + } + } + .imagePicker( + isShow: Binding( + get: { state.isPresentedProfileImage }, + set: { _ in intent.imagePickerDismissed() } + ), + pickedImageResult: Binding( + get: { .none }, + set: { intent.imageDidSelected(imageResult: $0) } + ) + ) + .cameraPicker( + isShow: Binding( + get: { state.isPresentedProfileCamera }, + set: { _ in intent.cameraDismissed() } + ), + pickedImageResult: Binding( + get: { .none }, + set: { intent.imageDidSelected(imageResult: $0) } + ) + ) + .imagePicker( + isShow: Binding( + get: { state.isPresentedPreviewImagePicker }, + set: { _ in intent.projectPreviewImagePickerDismissed() } + ), + pickedImageResult: Binding( + get: { .none }, + set: { + guard let image = $0 else { return } + intent.appendPreviewImage(index: state.focusedProjectIndex, image: image) + } + ) + ) + .fullScreenCover( + isPresented: Binding( + get: { state.isPresentedTechStackAppend }, + set: { _ in intent.techStackAppendDismissed() } + ) + ) { + DeferView { + techStackAppendBuildable.makeView(initial: state.techStacks) { techStacks in + intent.techStackAppendDidComplete(techStacks: techStacks) + } + .eraseToAnyView() + } + } + .fullScreenCover( + isPresented: Binding( + get: { state.isPresentedProjectTechStackAppend }, + set: { _ in intent.projectTechStackAppendDismissed() } + ) + ) { + DeferView { + techStackAppendBuildable.makeView( + initial: Array(state.projectList[safe: state.focusedProjectIndex]?.techStacks ?? []) + ) { + intent.techStacksDidSelect(index: state.focusedProjectIndex, techStacks: $0) + } + .eraseToAnyView() + } + } + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Scene/MyPageWorkInfoView.swift b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageWorkInfoView.swift new file mode 100644 index 00000000..83fe5dd6 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Scene/MyPageWorkInfoView.swift @@ -0,0 +1,84 @@ +import BaseFeature +import DesignSystem +import SwiftUI +import ViewUtil + +struct MyPageWorkInfoView: View { + @StateObject var container: MVIContainer + var intent: MyPageWorkInfoIntentProtocol { container.intent } + var state: MyPageWorkInfoStateProtocol { container.model } + + init(container: MVIContainer) { + self._container = StateObject(wrappedValue: container) + } + + var body: some View { + Section { + VStack(spacing: 24) { + SMSTextField( + "정규직", + text: Binding( + get: { state.formOfEmployment.display() }, + set: { _ in } + ), + isOnClear: false + ) + .disabled(true) + .overlay(alignment: .trailing) { + SMSIcon(.downChevron) + .padding(.trailing, 12) + } + .titleWrapper("희망 고용 형태") + .onTapGesture { + intent.formOfEmployeementSheetIsRequired() + } + + SMSTextField( + "희망 연봉 (10,000원 단위)", + text: Binding( + get: { state.salary }, + set: intent.updateSalary(salary:) + ) + ) + .keyboardType(.numberPad) + .titleWrapper("희망 연봉") + + workRegionList() + } + } header: { + SectionHeaderView(title: "근무 조건") + } + .padding(.horizontal, 20) + } + + @ViewBuilder + func workRegionList() -> some View { + VStack(spacing: 8) { + ForEach(state.workRegionList.indices, id: \.self) { index in + HStack(spacing: 16) { + SMSTextField( + "근무 희망 지역 입력", + text: Binding( + get: { state.workRegionList[safe: index] ?? "" }, + set: { intent.updateWorkRegion(region: $0, at: index) } + ) + ) + + Button { + intent.deleteWorkRegion(at: index) + } label: { + SMSIcon(.trash) + } + } + } + + SMSChip("추가") { + intent.appendWorkRegion() + } + .aligned(.leading) + } + .titleWrapper("근무 지역") + .aligned(.leading) + .animation(.default, value: state.workRegionList.count) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Scene/View/ImageMethodPickerView.swift b/Projects/Feature/MyPageFeature/Sources/Scene/View/ImageMethodPickerView.swift new file mode 100644 index 00000000..395e5477 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Scene/View/ImageMethodPickerView.swift @@ -0,0 +1,34 @@ +import DesignSystem +import SwiftUI + +struct ImageMethodPickerView: View { + private var albumAction: () -> Void + private var cameraAction: () -> Void + + init( + albumAction: @escaping () -> Void, + cameraAction: @escaping () -> Void + ) { + self.albumAction = albumAction + self.cameraAction = cameraAction + } + + var body: some View { + VStack(spacing: 28) { + Group { + ImageMethodRowView(title: "앨범에서 가져오기", icon: .photo) { + albumAction() + } + + ImageMethodRowView(title: "카메라에서 촬영하기", icon: .camera) { + cameraAction() + } + } + .buttonStyle(.plain) + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 20) + .padding(.top, 16) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Scene/View/ImageMethodRowView.swift b/Projects/Feature/MyPageFeature/Sources/Scene/View/ImageMethodRowView.swift new file mode 100644 index 00000000..12846ab9 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Scene/View/ImageMethodRowView.swift @@ -0,0 +1,32 @@ +import DesignSystem +import SwiftUI + +struct ImageMethodRowView: View { + private var title: String + private var icon: SMSIcon.Icon + private var action: () -> Void + + init( + title: String, + icon: SMSIcon.Icon, + action: @escaping () -> Void + ) { + self.title = title + self.icon = icon + self.action = action + } + + var body: some View { + Button { + action() + } label: { + Label { + SMSText(title, font: .body1) + } icon: { + SMSIcon(icon) + } + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.sms(.system(.white))) + } + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Scene/View/MajorRowView.swift b/Projects/Feature/MyPageFeature/Sources/Scene/View/MajorRowView.swift new file mode 100644 index 00000000..5ecc9f11 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Scene/View/MajorRowView.swift @@ -0,0 +1,26 @@ +import DesignSystem +import SwiftUI + +struct MajorRowView: View { + private var text: String + @Binding private var isSeleted: Bool + + init(text: String, isSeleted: Binding) { + self.text = text + _isSeleted = isSeleted + } + + var body: some View { + HStack { + SMSText(text, font: .body1) + + Spacer() + + SMSRadioButton(isSelected: $isSeleted) + .buttonWrapper {} + } + .padding(.horizontal, 20) + .padding(.vertical, 12) + .frame(maxWidth: .infinity, alignment: .leading) + } +} diff --git a/Projects/Feature/MyPageFeature/Sources/Scene/View/SectionHeaderView.swift b/Projects/Feature/MyPageFeature/Sources/Scene/View/SectionHeaderView.swift new file mode 100644 index 00000000..fc8a84c4 --- /dev/null +++ b/Projects/Feature/MyPageFeature/Sources/Scene/View/SectionHeaderView.swift @@ -0,0 +1,15 @@ +import DesignSystem +import SwiftUI + +struct SectionHeaderView: View { + let title: String + + var body: some View { + SMSText(title, font: .title1) + .aligned(.leading) + .padding(.vertical) + .background { + Color.sms(.system(.white)) + } + } +} diff --git a/Projects/Feature/MyPageFeature/Tests/MyPageFeatureTest.swift b/Projects/Feature/MyPageFeature/Tests/MyPageFeatureTest.swift new file mode 100644 index 00000000..13663bea --- /dev/null +++ b/Projects/Feature/MyPageFeature/Tests/MyPageFeatureTest.swift @@ -0,0 +1,11 @@ +import XCTest + +final class MyPageFeatureTests: XCTestCase { + override func setUpWithError() throws {} + + override func tearDownWithError() throws {} + + func testExample() { + XCTAssertEqual(1, 1) + } +} diff --git a/Projects/Feature/RootFeature/Sources/Model/RootModel.swift b/Projects/Feature/RootFeature/Sources/Model/RootModel.swift index a190ee71..db08c1ea 100644 --- a/Projects/Feature/RootFeature/Sources/Model/RootModel.swift +++ b/Projects/Feature/RootFeature/Sources/Model/RootModel.swift @@ -1,7 +1,7 @@ import Foundation final class RootModel: ObservableObject, RootStateProtocol { - @Published var sceneType: RootSceneType = .signin + @Published var sceneType: RootSceneType = .splash } extension RootModel: RootActionProtocol { diff --git a/Projects/Feature/StudentDetailFeature/Sources/Model/StudentDetailModel.swift b/Projects/Feature/StudentDetailFeature/Sources/Model/StudentDetailModel.swift index 53683190..7b7a5fdd 100644 --- a/Projects/Feature/StudentDetailFeature/Sources/Model/StudentDetailModel.swift +++ b/Projects/Feature/StudentDetailFeature/Sources/Model/StudentDetailModel.swift @@ -25,7 +25,6 @@ final class StudentDetailModel: ObservableObject, StudentDetailStateProtocol { } @Published var _studentDetailEntity: StudentDetailEntity? @Published var isLoading: Bool = false - @Published var isDownloading: Bool = false } // swiftlint: enable identifier_name @@ -41,8 +40,4 @@ extension StudentDetailModel: StudentDetailActionProtocol { func updateIsLoading(isLoading: Bool) { self.isLoading = isLoading } - - func updateIsDownloading(isDownloading: Bool) { - self.isDownloading = isDownloading - } } diff --git a/Projects/Feature/StudentDetailFeature/Sources/Model/StudentDetailModelProtocol.swift b/Projects/Feature/StudentDetailFeature/Sources/Model/StudentDetailModelProtocol.swift index ed14a12a..dbe02943 100644 --- a/Projects/Feature/StudentDetailFeature/Sources/Model/StudentDetailModelProtocol.swift +++ b/Projects/Feature/StudentDetailFeature/Sources/Model/StudentDetailModelProtocol.swift @@ -6,12 +6,10 @@ protocol StudentDetailStateProtocol { var userRole: UserRoleType { get } var studentDetailEntity: StudentDetailEntity? { get } var isLoading: Bool { get } - var isDownloading: Bool { get } } protocol StudentDetailActionProtocol: AnyObject { func updateUserRole(role: UserRoleType) func updateStudentDetailEntity(entity: StudentDetailEntity) func updateIsLoading(isLoading: Bool) - func updateIsDownloading(isDownloading: Bool) } diff --git a/Projects/Feature/StudentDetailFeature/Sources/Scene/StudentDetailView.swift b/Projects/Feature/StudentDetailFeature/Sources/Scene/StudentDetailView.swift index 17833886..660c6436 100644 --- a/Projects/Feature/StudentDetailFeature/Sources/Scene/StudentDetailView.swift +++ b/Projects/Feature/StudentDetailFeature/Sources/Scene/StudentDetailView.swift @@ -93,16 +93,6 @@ struct StudentDetailView: View { .onAppear { intent.onAppear() } - .smsToast( - text: "드림북을 다운로드 중입니다...", - isShowing: Binding( - get: { state.isDownloading }, - set: { _ in } - ) - ) { - LottieView(asset: .smsLoading) - .frame(width: 24, height: 24) - } .navigationBarHidden(true) } diff --git a/Projects/Feature/StudentDetailFeature/Sources/Scene/View/StudentDetailTitleWrapper.swift b/Projects/Feature/StudentDetailFeature/Sources/Scene/View/StudentDetailTitleWrapper.swift index fa5ea63c..a312ed05 100644 --- a/Projects/Feature/StudentDetailFeature/Sources/Scene/View/StudentDetailTitleWrapper.swift +++ b/Projects/Feature/StudentDetailFeature/Sources/Scene/View/StudentDetailTitleWrapper.swift @@ -4,10 +4,6 @@ import SwiftUI struct StudentDetailTitleWrapper: ViewModifier { let title: String - init(title: String) { - self.title = title - } - func body(content: Content) -> some View { VStack(alignment: .leading, spacing: 8) { Text(title) diff --git a/Scripts/CodeSigning.swift b/Scripts/CodeSigning.swift index 39bc29b8..3cf6c01f 100644 --- a/Scripts/CodeSigning.swift +++ b/Scripts/CodeSigning.swift @@ -1,7 +1,7 @@ #!/usr/bin/swift import Foundation -func handleSIGINT(_ signal: Int32) -> Void { +func handleSIGINT(_ signal: Int32) { exit(0) } @@ -32,5 +32,3 @@ public extension SettingsDictionary { writeContentInFile(path: "Tuist/ProjectDescriptionHelpers/CodeSign.swift", content: codeSignContent) print("✅ Code Sign extension generated successfully!") - - diff --git a/Scripts/GenerateFeature/Project.swift b/Scripts/GenerateFeature/Project.swift index 3cabc43e..46e7287a 100644 --- a/Scripts/GenerateFeature/Project.swift +++ b/Scripts/GenerateFeature/Project.swift @@ -1,9 +1,9 @@ -//import ProjectDescription -//import ProjectDescriptionHelpers +// import ProjectDescription +// import ProjectDescriptionHelpers // -//let project = Project.staticFramework( +// let project = Project.staticFramework( // name: "Feature", // dependencies: [ // .Project.Features.Feature // ] -//) +// ) diff --git a/Scripts/GenerateModule.swift b/Scripts/GenerateModule.swift index 21df0b24..f9272010 100644 --- a/Scripts/GenerateModule.swift +++ b/Scripts/GenerateModule.swift @@ -1,7 +1,7 @@ #!/usr/bin/swift import Foundation -func handleSIGINT(_ signal: Int32) -> Void { +func handleSIGINT(_ signal: Int32) { exit(0) } @@ -124,7 +124,7 @@ let project = Project.makeModule( """ writeContentInFile( - path: currentPath + "Projects/\(layer.rawValue)/\(moduleName)/Project.swift", + path: currentPath + "Projects/\(layer.rawValue)/\(moduleName)/Project.swift", content: projectSwift ) } @@ -135,14 +135,14 @@ func makeProjectDirectory() { func makeProjectScaffold(targetString: String) { _ = try? bash.run( - commandName: "tuist", + commandName: "tuist", arguments: ["scaffold", "Module", "--name", "\(moduleName)", "--layer", "\(layer.rawValue)", "--target", "\(targetString)"] ) } func makeScaffold(target: MicroTargetType) { _ = try? bash.run( - commandName: "tuist", + commandName: "tuist", arguments: ["scaffold", "\(target.rawValue)", "--name", "\(moduleName)", "--layer", "\(layer.rawValue)"] ) } @@ -162,7 +162,7 @@ func updateFileContent( guard let readHandle = try? FileHandle(forReadingFrom: fileURL) else { fatalError("❌ Failed to find \(filePath)") } - guard let readData = try? readHandle.readToEnd() else { + guard let readData = try? readHandle.readToEnd() else { fatalError("❌ Failed to find \(filePath)") } try? readHandle.close() @@ -178,14 +178,13 @@ func updateFileContent( try? writeHandle.close() } - // MARK: - Starting point print("Enter layer name\n(Feature | Domain | Core | Shared)", terminator: " : ") let layerInput = readLine() -guard - let layerInput, - !layerInput.isEmpty , +guard + let layerInput, + !layerInput.isEmpty, let layerUnwrapping = LayerType(rawValue: layerInput) else { print("Layer is empty or invalid") @@ -230,7 +229,6 @@ print("interface: \(hasInterface), testing: \(hasTesting), unitTests: \(hasUnitT print("------------------------------------------------------------------------------------------------------------------------") print("✅ Module is created successfully!") - // MARK: - Bash protocol CommandExecuting { func run(commandName: String, arguments: [String]) throws -> String @@ -246,7 +244,7 @@ struct Bash: CommandExecuting { } private func resolve(_ command: String) throws -> String { - guard var bashCommand = try? run("/bin/bash" , with: ["-l", "-c", "which \(command)"]) else { + guard var bashCommand = try? run("/bin/bash", with: ["-l", "-c", "which \(command)"]) else { throw BashError.commandNotFound(name: command) } bashCommand = bashCommand.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines) @@ -265,4 +263,3 @@ struct Bash: CommandExecuting { return output } } - diff --git a/Scripts/InitEnvironment.swift b/Scripts/InitEnvironment.swift index 132cf78e..6e2f3a26 100644 --- a/Scripts/InitEnvironment.swift +++ b/Scripts/InitEnvironment.swift @@ -1,7 +1,7 @@ #!/usr/bin/swift import Foundation -func handleSIGINT(_ signal: Int32) -> Void { +func handleSIGINT(_ signal: Int32) { exit(0) } diff --git a/Scripts/NewDependency.swift b/Scripts/NewDependency.swift index dcdf6ea6..561eccbb 100644 --- a/Scripts/NewDependency.swift +++ b/Scripts/NewDependency.swift @@ -1,7 +1,7 @@ #!/usr/bin/swift import Foundation -func handleSIGINT(_ signal: Int32) -> Void { +func handleSIGINT(_ signal: Int32) { exit(0) } @@ -18,7 +18,7 @@ signal(SIGINT, handleSIGINT) guard let readHandle = try? FileHandle(forReadingFrom: fileURL) else { fatalError("❌ Failed to find \(filePath)") } - guard let readData = try? readHandle.readToEnd() else { + guard let readData = try? readHandle.readToEnd() else { fatalError("❌ Failed to find \(filePath)") } try? readHandle.close() @@ -74,4 +74,3 @@ signal(SIGINT, handleSIGINT) } registerDependency(name: dependencyName, package: packageName, url: dependencyURL, version: dependencyVersion) - diff --git a/Tuist/Dependencies.swift b/Tuist/Dependencies.swift index 34877fbe..7dd13af0 100644 --- a/Tuist/Dependencies.swift +++ b/Tuist/Dependencies.swift @@ -12,8 +12,8 @@ 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.11")), - + .remote(url: "https://github.com/GSM-MSG/Emdpoint.git", requirement: .exact("3.5.0")) + ], baseSettings: .settings( configurations: [ diff --git a/Tuist/ProjectDescriptionHelpers/Project+Template.swift b/Tuist/ProjectDescriptionHelpers/Project+Template.swift index a8befb61..bf9c99f6 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Template.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Template.swift @@ -178,7 +178,7 @@ public extension Project { infoPlist: .extendingDefault(with: [ "UIMainStoryboardFile": "", "UILaunchStoryboardName": "LaunchScreen", - "ENABLE_TESTS": .boolean(true), + "ENABLE_TESTS": .boolean(true) ]), sources: .demoSources, resources: ["Demo/Resources/**"], diff --git a/Tuist/Templates/Tests/Tests.swift b/Tuist/Templates/Tests/Tests.swift index 09d79c9e..b5d64a60 100644 --- a/Tuist/Templates/Tests/Tests.swift +++ b/Tuist/Templates/Tests/Tests.swift @@ -13,6 +13,6 @@ private let template = Template( .file( path: "Projects/\(layerAttribute)/\(nameAttribute)/Tests/\(nameAttribute)Test.swift", templatePath: "Tests.stencil" - ), + ) ] ) diff --git a/Tuist/Templates/UITests/UITests.swift b/Tuist/Templates/UITests/UITests.swift index 716ef246..503d283e 100644 --- a/Tuist/Templates/UITests/UITests.swift +++ b/Tuist/Templates/UITests/UITests.swift @@ -13,6 +13,6 @@ private let template = Template( .file( path: "Projects/\(layerAttribute)/\(nameAttribute)/UITests/\(nameAttribute)UITests.swift", templatePath: "UITests.stencil" - ), + ) ] )