From 4b63a940932d4570e3b61ee10726fa7522133b1f Mon Sep 17 00:00:00 2001 From: Sunghun Kim <81547954+kimsh153@users.noreply.github.com> Date: Wed, 12 Jun 2024 01:27:24 +0900 Subject: [PATCH 01/13] :construction: :: wip --- .../Dependency+Target.swift | 12 ++ .../ModulePaths.swift | 1 + Projects/App/Project.swift | 1 + .../Sources/Application/DI/AppComponent.swift | 12 ++ .../Sources/Application/NeedleGenerated.swift | 100 ++++++++---- Projects/App/Support/Info.plist | 5 + .../TitleWrapperViewModifier.swift | 18 ++- .../DI/AuthenticationDomainBuildable.swift | 4 + .../InputAuthenticationRequestDTO.swift | 34 +++++ .../RemoteAuthenticationDataSource.swift | 7 + .../Entity/AuthenticationFormEntity.swift | 31 ++++ .../Interface/Entity/FieldEntity.swift | 15 ++ .../Interface/Entity/SectionEntity.swift | 13 ++ .../Interface/Entity/ValueEntity.swift | 9 ++ .../Interface/Enum/FieldEnum.swift | 8 + .../Error/AuthenticationDomainError.swift | 14 ++ .../Repository/AuthenticationRepository.swift | 6 + .../FetchAuthenticationFormUseCase.swift | 5 + .../UseCase/InputAuthenticationUseCase.swift | 5 + .../Domain/AuthenticationDomain/Project.swift | 12 ++ .../DI/AuthenticationDomainComponent.swift | 23 +++ .../DTO/Response/ContentsResponseDTO.swift | 92 +++++++++++ .../RemoteAuthenticationDataSourceImpl.swift | 12 ++ .../Endpoint/AuthenticationEndpoint.swift | 64 ++++++++ .../AuthenticationRepositoryImpl.swift | 17 +++ .../FetchAuthenticationFormUseCaseImpl.swift | 13 ++ .../InputAuthenticationFormUseCaseImpl.swift | 13 ++ .../Testing/Testing.swift} | 0 .../BaseDomain/Sources/Base/SMSDomain.swift | 1 + .../Demo/Sources/AppDelegate.swift | 53 ++++--- .../GSMAuthenticationBuildable.swift | 6 + .../Interface/GSMAuthenticationDelegate.swift | 2 + .../DI/GSMAuthenticationComponent.swift | 29 ++++ .../Intent/GSMAuthenticationFormIntent.swift | 143 +++++++++++++++++- .../GSMAuthenticationFormIntentProtocol.swift | 5 +- .../GSMAuthenticationFormFieldModel.swift | 10 ++ .../Model/GSMAuthenticationFormModel.swift | 16 +- .../GSMAuthenticationFormModelProtocol.swift | 7 +- .../Model/GSMAuthenticationFormUIModel.swift | 7 +- .../GSMAuthenticationFormBuilderView.swift | 72 +++++---- .../Scene/GSMAuthenticationFormView.swift | 36 ++++- Projects/Feature/MainFeature/Project.swift | 1 + .../Sources/DI/MainComponent.swift | 5 +- .../Sources/Intent/MainIntentProtocol.swift | 3 +- .../MainFeature/Sources/Scene/MainView.swift | 8 +- 45 files changed, 846 insertions(+), 104 deletions(-) create mode 100644 Projects/Domain/AuthenticationDomain/Interface/DI/AuthenticationDomainBuildable.swift create mode 100644 Projects/Domain/AuthenticationDomain/Interface/DTO/Request/InputAuthenticationRequestDTO.swift create mode 100644 Projects/Domain/AuthenticationDomain/Interface/DataSource/RemoteAuthenticationDataSource.swift create mode 100644 Projects/Domain/AuthenticationDomain/Interface/Entity/AuthenticationFormEntity.swift create mode 100644 Projects/Domain/AuthenticationDomain/Interface/Entity/FieldEntity.swift create mode 100644 Projects/Domain/AuthenticationDomain/Interface/Entity/SectionEntity.swift create mode 100644 Projects/Domain/AuthenticationDomain/Interface/Entity/ValueEntity.swift create mode 100644 Projects/Domain/AuthenticationDomain/Interface/Enum/FieldEnum.swift create mode 100644 Projects/Domain/AuthenticationDomain/Interface/Error/AuthenticationDomainError.swift create mode 100644 Projects/Domain/AuthenticationDomain/Interface/Repository/AuthenticationRepository.swift create mode 100644 Projects/Domain/AuthenticationDomain/Interface/UseCase/FetchAuthenticationFormUseCase.swift create mode 100644 Projects/Domain/AuthenticationDomain/Interface/UseCase/InputAuthenticationUseCase.swift create mode 100644 Projects/Domain/AuthenticationDomain/Project.swift create mode 100644 Projects/Domain/AuthenticationDomain/Sources/DI/AuthenticationDomainComponent.swift create mode 100644 Projects/Domain/AuthenticationDomain/Sources/DTO/Response/ContentsResponseDTO.swift create mode 100644 Projects/Domain/AuthenticationDomain/Sources/DataSource/RemoteAuthenticationDataSourceImpl.swift create mode 100644 Projects/Domain/AuthenticationDomain/Sources/Endpoint/AuthenticationEndpoint.swift create mode 100644 Projects/Domain/AuthenticationDomain/Sources/Repository/AuthenticationRepositoryImpl.swift create mode 100644 Projects/Domain/AuthenticationDomain/Sources/UseCase/FetchAuthenticationFormUseCaseImpl.swift create mode 100644 Projects/Domain/AuthenticationDomain/Sources/UseCase/InputAuthenticationFormUseCaseImpl.swift rename Projects/{Feature/GSMAuthenticationFormFeature/Interface/Interface.swift => Domain/AuthenticationDomain/Testing/Testing.swift} (100%) create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Interface/GSMAuthenticationBuildable.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Interface/GSMAuthenticationDelegate.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/DI/GSMAuthenticationComponent.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormFieldModel.swift diff --git a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift index bde3afbc..979d23d4 100644 --- a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift +++ b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift @@ -184,6 +184,18 @@ public extension TargetDependency.Feature { } public extension TargetDependency.Domain { + static let AuthenticationDomainTesting = TargetDependency.project( + target: ModulePaths.Domain.AuthenticationDomain.targetName(type: .testing), + path: .relativeToDomain(ModulePaths.Domain.AuthenticationDomain.rawValue) + ) + static let AuthenticationDomainInterface = TargetDependency.project( + target: ModulePaths.Domain.AuthenticationDomain.targetName(type: .interface), + path: .relativeToDomain(ModulePaths.Domain.AuthenticationDomain.rawValue) + ) + static let AuthenticationDomain = TargetDependency.project( + target: ModulePaths.Domain.AuthenticationDomain.targetName(type: .sources), + path: .relativeToDomain(ModulePaths.Domain.AuthenticationDomain.rawValue) + ) static let TeacherDomainTesting = TargetDependency.project( target: ModulePaths.Domain.TeacherDomain.targetName(type: .testing), path: .relativeToDomain(ModulePaths.Domain.TeacherDomain.rawValue) diff --git a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift index c417767c..a5c6dbb9 100644 --- a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift +++ b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift @@ -41,6 +41,7 @@ public extension ModulePaths { public extension ModulePaths { enum Domain: String { + case AuthenticationDomain case TeacherDomain case UserDomain case TechStackDomain diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index ae7c7c78..e8809fe4 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -62,6 +62,7 @@ let targets: [Target] = [ .Domain.UserDomain, .Domain.TechStackDomain, .Domain.TeacherDomain, + .Domain.AuthenticationDomain, .Core.JwtStore, .Shared.KeychainModule ], diff --git a/Projects/App/Sources/Application/DI/AppComponent.swift b/Projects/App/Sources/Application/DI/AppComponent.swift index b3c6f452..7f6410d1 100644 --- a/Projects/App/Sources/Application/DI/AppComponent.swift +++ b/Projects/App/Sources/Application/DI/AppComponent.swift @@ -1,10 +1,14 @@ import AuthDomain import AuthDomainInterface +import AuthenticationDomain +import AuthenticationDomainInterface import BaseDomain import FileDomain import FileDomainInterface import FilterFeature import FilterFeatureInterface +import GSMAuthenticationFormFeature +import GSMAuthenticationFormFeatureInterface import InputCertificateInfoFeature import InputCertificateInfoFeatureInterface import InputInformationFeature @@ -124,6 +128,10 @@ final class AppComponent: BootstrapComponent { SplashComponent(parent: self) } + public var gsmAuthenticationBuildable: any GSMAuthenticationBuildable { + GSMAuthenticationComponent(parent: self) + } + public var authDomainBuildable: any AuthDomainBuildable { AuthDomainComponent(parent: self) } @@ -155,4 +163,8 @@ final class AppComponent: BootstrapComponent { public var keychainBuildable: any KeychainBuildable { KeychainComponent(parent: self) } + + public var authenticationDomainBuildable: any AuthenticationDomainBuildable { + AuthenticationDomainComponent(parent: self) + } } diff --git a/Projects/App/Sources/Application/NeedleGenerated.swift b/Projects/App/Sources/Application/NeedleGenerated.swift index a03a72a7..f29b6191 100644 --- a/Projects/App/Sources/Application/NeedleGenerated.swift +++ b/Projects/App/Sources/Application/NeedleGenerated.swift @@ -2,6 +2,8 @@ import AuthDomain import AuthDomainInterface +import AuthenticationDomain +import AuthenticationDomainInterface import BaseDomain import BaseFeature import FileDomain @@ -9,6 +11,8 @@ import FileDomainInterface import FilterFeature import FilterFeatureInterface import Foundation +import GSMAuthenticationFormFeature +import GSMAuthenticationFormFeatureInterface import InputAuthenticationFeatureInterface import InputCertificateInfoFeature import InputCertificateInfoFeatureInterface @@ -84,6 +88,19 @@ private class JwtStoreDependency5613ee3d4fea5093f6faProvider: JwtStoreDependency private func factoryb27d5aae1eb7e73575a6f47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { return JwtStoreDependency5613ee3d4fea5093f6faProvider(appComponent: parent1(component) as! AppComponent) } +private class GSMAuthenticationDependencydf257bda3051cc91534fProvider: GSMAuthenticationDependency { + var authenticationDomainBuildable: any AuthenticationDomainBuildable { + return appComponent.authenticationDomainBuildable + } + private let appComponent: AppComponent + init(appComponent: AppComponent) { + self.appComponent = appComponent + } +} +/// ^->AppComponent->GSMAuthenticationComponent +private func factorye9687e0765e19ddd651cf47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { + return GSMAuthenticationDependencydf257bda3051cc91534fProvider(appComponent: parent1(component) as! AppComponent) +} private class SplashDependencye0cb7136f2ec3edfd60aProvider: SplashDependency { var authDomainBuildable: any AuthDomainBuildable { return appComponent.authDomainBuildable @@ -162,6 +179,9 @@ private class MainDependency7c6a5b4738b211b8e155Provider: MainDependency { var studentDetailBuildable: any StudentDetailBuildable { return appComponent.studentDetailBuildable } + var gsmAuthenticationBuildable: any GSMAuthenticationBuildable { + return appComponent.gsmAuthenticationBuildable + } var userDomainBuildable: any UserDomainBuildable { return appComponent.userDomainBuildable } @@ -389,6 +409,19 @@ private class TechStackDomainDependencyc7e8371994569e951d57Provider: TechStackDo private func factory254149359ff45b2db35bf47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { return TechStackDomainDependencyc7e8371994569e951d57Provider(appComponent: parent1(component) as! AppComponent) } +private class AuthenticationDomainDependency304198d4e760c1e9976dProvider: AuthenticationDomainDependency { + var jwtStoreBuildable: any JwtStoreBuildable { + return appComponent.jwtStoreBuildable + } + private let appComponent: AppComponent + init(appComponent: AppComponent) { + self.appComponent = appComponent + } +} +/// ^->AppComponent->AuthenticationDomainComponent +private func factory399be911d3fd8d0d070af47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { + return AuthenticationDomainDependency304198d4e760c1e9976dProvider(appComponent: parent1(component) as! AppComponent) +} private class AuthDomainDependency4518b8977185a5c9ff71Provider: AuthDomainDependency { var jwtStoreBuildable: any JwtStoreBuildable { return appComponent.jwtStoreBuildable @@ -438,31 +471,33 @@ extension JwtStoreComponent: Registration { extension AppComponent: Registration { public func registerItems() { - localTable["rootComponent-RootComponent"] = { [unowned self] in self.rootComponent as Any } - localTable["signinBuildable-any SigninBuildable"] = { [unowned self] in self.signinBuildable as Any } - localTable["inputInformationBuildable-any InputInformationBuildable"] = { [unowned self] in self.inputInformationBuildable as Any } - localTable["inputProfileInfoBuildable-any InputProfileInfoBuildable"] = { [unowned self] in self.inputProfileInfoBuildable as Any } - localTable["inputSchoolLifeInfoBuildable-any InputSchoolLifeInfoBuildable"] = { [unowned self] in self.inputSchoolLifeInfoBuildable as Any } - localTable["inputWorkInfoBuildable-any InputWorkInfoBuildable"] = { [unowned self] in self.inputWorkInfoBuildable as Any } - localTable["inputMilitaryInfoBuildable-any InputMilitaryInfoBuildable"] = { [unowned self] in self.inputMilitaryInfoBuildable as Any } - localTable["inputCertificateInfoBuildable-any InputCertificateInfoBuildable"] = { [unowned self] in self.inputCertificateInfoBuildable as Any } - localTable["inputLanguageInfoBuildable-any InputLanguageInfoBuildable"] = { [unowned self] in self.inputLanguageInfoBuildable as Any } - localTable["inputPrizeInfoBuildable-any InputPrizeInfoBuildable"] = { [unowned self] in self.inputPrizeInfoBuildable as Any } - localTable["inputProjectInfoBuildable-any InputProjectInfoBuildable"] = { [unowned self] in self.inputProjectInfoBuildable as Any } - localTable["mainBuildable-any MainBuildable"] = { [unowned self] in self.mainBuildable as Any } - localTable["myPageBuildable-any MyPageBuildable"] = { [unowned self] in self.myPageBuildable as Any } - localTable["techStackAppendBuildable-any TechStackAppendBuildable"] = { [unowned self] in self.techStackAppendBuildable as Any } - localTable["studentDetailBuildable-any StudentDetailBuildable"] = { [unowned self] in self.studentDetailBuildable as Any } - localTable["filterBuildable-any FilterBuildable"] = { [unowned self] in self.filterBuildable as Any } - localTable["splashBuildable-any SplashBuildable"] = { [unowned self] in self.splashBuildable as Any } - localTable["authDomainBuildable-any AuthDomainBuildable"] = { [unowned self] in self.authDomainBuildable as Any } - localTable["studentDomainBuildable-any StudentDomainBuildable"] = { [unowned self] in self.studentDomainBuildable as Any } - localTable["majorDomainBuildable-any MajorDomainBuildable"] = { [unowned self] in self.majorDomainBuildable as Any } - localTable["fileDomainBuildable-any FileDomainBuildable"] = { [unowned self] in self.fileDomainBuildable as Any } - localTable["userDomainBuildable-any UserDomainBuildable"] = { [unowned self] in self.userDomainBuildable as Any } - localTable["techStackDomainBuildable-any TechStackDomainBuildable"] = { [unowned self] in self.techStackDomainBuildable as Any } - localTable["jwtStoreBuildable-any JwtStoreBuildable"] = { [unowned self] in self.jwtStoreBuildable as Any } - localTable["keychainBuildable-any KeychainBuildable"] = { [unowned self] in self.keychainBuildable as Any } + localTable["rootComponent-RootComponent"] = { self.rootComponent as Any } + localTable["signinBuildable-any SigninBuildable"] = { self.signinBuildable as Any } + localTable["inputInformationBuildable-any InputInformationBuildable"] = { self.inputInformationBuildable as Any } + localTable["inputProfileInfoBuildable-any InputProfileInfoBuildable"] = { self.inputProfileInfoBuildable as Any } + localTable["inputSchoolLifeInfoBuildable-any InputSchoolLifeInfoBuildable"] = { self.inputSchoolLifeInfoBuildable as Any } + localTable["inputWorkInfoBuildable-any InputWorkInfoBuildable"] = { self.inputWorkInfoBuildable as Any } + localTable["inputMilitaryInfoBuildable-any InputMilitaryInfoBuildable"] = { self.inputMilitaryInfoBuildable as Any } + localTable["inputCertificateInfoBuildable-any InputCertificateInfoBuildable"] = { self.inputCertificateInfoBuildable as Any } + localTable["inputLanguageInfoBuildable-any InputLanguageInfoBuildable"] = { self.inputLanguageInfoBuildable as Any } + localTable["inputPrizeInfoBuildable-any InputPrizeInfoBuildable"] = { self.inputPrizeInfoBuildable as Any } + localTable["inputProjectInfoBuildable-any InputProjectInfoBuildable"] = { self.inputProjectInfoBuildable as Any } + localTable["mainBuildable-any MainBuildable"] = { self.mainBuildable as Any } + localTable["myPageBuildable-any MyPageBuildable"] = { self.myPageBuildable as Any } + localTable["techStackAppendBuildable-any TechStackAppendBuildable"] = { self.techStackAppendBuildable as Any } + localTable["studentDetailBuildable-any StudentDetailBuildable"] = { self.studentDetailBuildable as Any } + localTable["filterBuildable-any FilterBuildable"] = { self.filterBuildable as Any } + localTable["splashBuildable-any SplashBuildable"] = { self.splashBuildable as Any } + localTable["gsmAuthenticationBuildable-any GSMAuthenticationBuildable"] = { self.gsmAuthenticationBuildable as Any } + localTable["authDomainBuildable-any AuthDomainBuildable"] = { self.authDomainBuildable as Any } + localTable["studentDomainBuildable-any StudentDomainBuildable"] = { self.studentDomainBuildable as Any } + localTable["majorDomainBuildable-any MajorDomainBuildable"] = { self.majorDomainBuildable as Any } + localTable["fileDomainBuildable-any FileDomainBuildable"] = { self.fileDomainBuildable as Any } + localTable["userDomainBuildable-any UserDomainBuildable"] = { self.userDomainBuildable as Any } + localTable["techStackDomainBuildable-any TechStackDomainBuildable"] = { self.techStackDomainBuildable as Any } + localTable["jwtStoreBuildable-any JwtStoreBuildable"] = { self.jwtStoreBuildable as Any } + localTable["keychainBuildable-any KeychainBuildable"] = { self.keychainBuildable as Any } + localTable["authenticationDomainBuildable-any AuthenticationDomainBuildable"] = { self.authenticationDomainBuildable as Any } } } extension KeychainComponent: Registration { @@ -470,6 +505,11 @@ extension KeychainComponent: Registration { } } +extension GSMAuthenticationComponent: Registration { + public func registerItems() { + keyPathToName[\GSMAuthenticationDependency.authenticationDomainBuildable] = "authenticationDomainBuildable-any AuthenticationDomainBuildable" + } +} extension SplashComponent: Registration { public func registerItems() { keyPathToName[\SplashDependency.authDomainBuildable] = "authDomainBuildable-any AuthDomainBuildable" @@ -501,6 +541,7 @@ extension MainComponent: Registration { keyPathToName[\MainDependency.filterBuildable] = "filterBuildable-any FilterBuildable" keyPathToName[\MainDependency.myPageBuildable] = "myPageBuildable-any MyPageBuildable" keyPathToName[\MainDependency.studentDetailBuildable] = "studentDetailBuildable-any StudentDetailBuildable" + keyPathToName[\MainDependency.gsmAuthenticationBuildable] = "gsmAuthenticationBuildable-any GSMAuthenticationBuildable" keyPathToName[\MainDependency.userDomainBuildable] = "userDomainBuildable-any UserDomainBuildable" } } @@ -589,6 +630,11 @@ extension TechStackDomainComponent: Registration { keyPathToName[\TechStackDomainDependency.jwtStoreBuildable] = "jwtStoreBuildable-any JwtStoreBuildable" } } +extension AuthenticationDomainComponent: Registration { + public func registerItems() { + keyPathToName[\AuthenticationDomainDependency.jwtStoreBuildable] = "jwtStoreBuildable-any JwtStoreBuildable" + } +} extension AuthDomainComponent: Registration { public func registerItems() { keyPathToName[\AuthDomainDependency.jwtStoreBuildable] = "jwtStoreBuildable-any JwtStoreBuildable" @@ -619,10 +665,11 @@ private func registerProviderFactory(_ componentPath: String, _ factory: @escapi #if !NEEDLE_DYNAMIC -@inline(never) private func register1() { +private func register1() { registerProviderFactory("^->AppComponent->JwtStoreComponent", factoryb27d5aae1eb7e73575a6f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent", factoryEmptyDependencyProvider) registerProviderFactory("^->AppComponent->KeychainComponent", factoryEmptyDependencyProvider) + registerProviderFactory("^->AppComponent->GSMAuthenticationComponent", factorye9687e0765e19ddd651cf47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->SplashComponent", factoryace9f05f51d68f4c0677f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->InputProjectInfoComponent", factory2378736e5949c5e8e9f4f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->MyPageComponent", factory0f6f456ebf157d02dfb3f47b58f8f304c97af4d5) @@ -643,6 +690,7 @@ private func registerProviderFactory(_ componentPath: String, _ factory: @escapi registerProviderFactory("^->AppComponent->FileDomainComponent", factoryd99c631e7a9c4984df37f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->StudentDomainComponent", factory2686a7e321a220c3265af47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->TechStackDomainComponent", factory254149359ff45b2db35bf47b58f8f304c97af4d5) + registerProviderFactory("^->AppComponent->AuthenticationDomainComponent", factory399be911d3fd8d0d070af47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->AuthDomainComponent", factoryc9b20c320bb79402d4c1f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->MajorDomainComponent", factoryc6563cd3e82b012ec3bef47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->UserDomainComponent", factory46488402f315d7f9530cf47b58f8f304c97af4d5) diff --git a/Projects/App/Support/Info.plist b/Projects/App/Support/Info.plist index 0b2b26a4..7edb0add 100644 --- a/Projects/App/Support/Info.plist +++ b/Projects/App/Support/Info.plist @@ -2,6 +2,11 @@ + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/Projects/Core/DesignSystem/Sources/TitleWrapperView/TitleWrapperViewModifier.swift b/Projects/Core/DesignSystem/Sources/TitleWrapperView/TitleWrapperViewModifier.swift index f493987a..71585013 100644 --- a/Projects/Core/DesignSystem/Sources/TitleWrapperView/TitleWrapperViewModifier.swift +++ b/Projects/Core/DesignSystem/Sources/TitleWrapperView/TitleWrapperViewModifier.swift @@ -3,13 +3,19 @@ import SwiftUI public struct TitleWrapperViewModifier: ViewModifier { var text: String var titlePosition: TitlePosition + var font: Font.SMSFontSystem + var color: Color.SMSColorSystem public init( _ text: String, - position: TitlePosition = .top(.leading) + position: TitlePosition = .top(.leading), + font: Font.SMSFontSystem = .body2, + color: Color.SMSColorSystem = .neutral(.n40) ) { self.text = text self.titlePosition = position + self.font = font + self.color = color } public func body(content: Content) -> some View { @@ -17,7 +23,7 @@ public struct TitleWrapperViewModifier: ViewModifier { case let .top(alignment): VStack(alignment: alignment.toSUI, spacing: 8) { Text(text) - .smsFont(.body2, color: .neutral(.n40)) + .smsFont(font, color: color) content } @@ -27,7 +33,7 @@ public struct TitleWrapperViewModifier: ViewModifier { content Text(text) - .smsFont(.body2, color: .neutral(.n40)) + .smsFont(font, color: color) } } } @@ -36,8 +42,10 @@ public struct TitleWrapperViewModifier: ViewModifier { public extension View { func titleWrapper( _ text: String, - position: TitlePosition = .top(.leading) + position: TitlePosition = .top(.leading), + font: Font.SMSFontSystem = .body2, + color: Color.SMSColorSystem = .neutral(.n40) ) -> some View { - modifier(TitleWrapperViewModifier(text, position: position)) + modifier(TitleWrapperViewModifier(text, position: position, font: font, color: color)) } } diff --git a/Projects/Domain/AuthenticationDomain/Interface/DI/AuthenticationDomainBuildable.swift b/Projects/Domain/AuthenticationDomain/Interface/DI/AuthenticationDomainBuildable.swift new file mode 100644 index 00000000..289ef321 --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Interface/DI/AuthenticationDomainBuildable.swift @@ -0,0 +1,4 @@ +public protocol AuthenticationDomainBuildable { + var fetchAuthenticationFormUseCase: any FetchAuthenticationFormUseCase { get } + var inputAuthenticationUseCase: any InputAuthenticationUseCase { get } +} diff --git a/Projects/Domain/AuthenticationDomain/Interface/DTO/Request/InputAuthenticationRequestDTO.swift b/Projects/Domain/AuthenticationDomain/Interface/DTO/Request/InputAuthenticationRequestDTO.swift new file mode 100644 index 00000000..0d64ca9a --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Interface/DTO/Request/InputAuthenticationRequestDTO.swift @@ -0,0 +1,34 @@ +import Foundation + +public struct InputAuthenticationRequestDTO: Encodable { + public let contents: [Content] + + public init(contents: [Content]) { + self.contents = contents + } +} + +public struct Content: Encodable { + public let sectionID: String + public let objects: [Object] + + public init(sectionID: String, objects: [Object]) { + self.sectionID = sectionID + self.objects = objects + } +} + +// MARK: - Object +public struct Object: Encodable { + public let fieldID: String + public let sectionType: FieldType + public let value: String + public let selectID: String + + public init(fieldID: String, sectionType: FieldType, value: String, selectID: String) { + self.fieldID = fieldID + self.sectionType = sectionType + self.value = value + self.selectID = selectID + } +} diff --git a/Projects/Domain/AuthenticationDomain/Interface/DataSource/RemoteAuthenticationDataSource.swift b/Projects/Domain/AuthenticationDomain/Interface/DataSource/RemoteAuthenticationDataSource.swift new file mode 100644 index 00000000..97bfb08a --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Interface/DataSource/RemoteAuthenticationDataSource.swift @@ -0,0 +1,7 @@ +import Foundation + +public protocol RemoteAuthenticationDataSource { + func fetchAuthenticationForm(uuid: String) async throws -> AuthenticationFormEntity + + func inputAuthentication(uuid: String, req: InputAuthenticationRequestDTO) async throws +} diff --git a/Projects/Domain/AuthenticationDomain/Interface/Entity/AuthenticationFormEntity.swift b/Projects/Domain/AuthenticationDomain/Interface/Entity/AuthenticationFormEntity.swift new file mode 100644 index 00000000..9eb5aaeb --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Interface/Entity/AuthenticationFormEntity.swift @@ -0,0 +1,31 @@ +import Foundation + +public struct AuthenticationFormEntity { + public let files: [File] + public let areas: [Area] + + public init(files: [File], areas: [Area]) { + self.files = files + self.areas = areas + } +} + +extension AuthenticationFormEntity { + public struct Area { + public let title: String + public let sections: [SectionEntity] + public init(title: String, sections: [SectionEntity]) { + self.title = title + self.sections = sections + } + } + + public struct File { + public let name: String + public let url: String + public init(name: String, url: String) { + self.name = name + self.url = url + } + } +} diff --git a/Projects/Domain/AuthenticationDomain/Interface/Entity/FieldEntity.swift b/Projects/Domain/AuthenticationDomain/Interface/Entity/FieldEntity.swift new file mode 100644 index 00000000..e756d7c4 --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Interface/Entity/FieldEntity.swift @@ -0,0 +1,15 @@ +public struct FieldEntity { + public let fieldId: String + public let type: FieldType + public let scoreDescription: String? + public let values: [ValueEntity]? + public let placeholder: String? + + public init(fieldId: String, type: FieldType, scoreDescription: String?, values: [ValueEntity]?, placeholder: String?) { + self.fieldId = fieldId + self.type = type + self.scoreDescription = scoreDescription + self.values = values + self.placeholder = placeholder + } +} diff --git a/Projects/Domain/AuthenticationDomain/Interface/Entity/SectionEntity.swift b/Projects/Domain/AuthenticationDomain/Interface/Entity/SectionEntity.swift new file mode 100644 index 00000000..be1c4c3a --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Interface/Entity/SectionEntity.swift @@ -0,0 +1,13 @@ +public struct SectionEntity { + public let sectionId: String + public let title: String + public let maxCount: Double + public let fields: [FieldEntity] + + public init(sectionId: String, title: String, maxCount: Double, fields: [FieldEntity]) { + self.sectionId = sectionId + self.title = title + self.maxCount = maxCount + self.fields = fields + } +} diff --git a/Projects/Domain/AuthenticationDomain/Interface/Entity/ValueEntity.swift b/Projects/Domain/AuthenticationDomain/Interface/Entity/ValueEntity.swift new file mode 100644 index 00000000..913d378f --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Interface/Entity/ValueEntity.swift @@ -0,0 +1,9 @@ +public struct ValueEntity { + public let selectId: String + public let value: String + + public init(selectId: String, value: String) { + self.selectId = selectId + self.value = value + } +} diff --git a/Projects/Domain/AuthenticationDomain/Interface/Enum/FieldEnum.swift b/Projects/Domain/AuthenticationDomain/Interface/Enum/FieldEnum.swift new file mode 100644 index 00000000..8a6f6dbf --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Interface/Enum/FieldEnum.swift @@ -0,0 +1,8 @@ +public enum FieldType: String, Codable { + case text = "TEXT" + case number = "NUMBER" + case boolean = "BOOLEAN" + case file = "FILE" + case select = "SELECT" +// case selectValue = "SELECT_VALUE" +} diff --git a/Projects/Domain/AuthenticationDomain/Interface/Error/AuthenticationDomainError.swift b/Projects/Domain/AuthenticationDomain/Interface/Error/AuthenticationDomainError.swift new file mode 100644 index 00000000..f6517aa0 --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Interface/Error/AuthenticationDomainError.swift @@ -0,0 +1,14 @@ +import Foundation + +public enum AuthenticationDomainError: Error { + case internalServerError +} + +extension AuthenticationDomainError: LocalizedError { + public var errorDescription: String? { + switch self { + case .internalServerError: + return "서버에서 문제가 발생하였습니다. 지속될 시 문의해주세요." + } + } +} diff --git a/Projects/Domain/AuthenticationDomain/Interface/Repository/AuthenticationRepository.swift b/Projects/Domain/AuthenticationDomain/Interface/Repository/AuthenticationRepository.swift new file mode 100644 index 00000000..f7ccc4d3 --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Interface/Repository/AuthenticationRepository.swift @@ -0,0 +1,6 @@ +import Foundation + +public protocol AuthenticationRepository { + func fetchAuthenticationForm(uuid: String) async throws -> AuthenticationFormEntity + func inputAuthentication(uuid: String, req: InputAuthenticationRequestDTO) async throws +} diff --git a/Projects/Domain/AuthenticationDomain/Interface/UseCase/FetchAuthenticationFormUseCase.swift b/Projects/Domain/AuthenticationDomain/Interface/UseCase/FetchAuthenticationFormUseCase.swift new file mode 100644 index 00000000..c2929896 --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Interface/UseCase/FetchAuthenticationFormUseCase.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol FetchAuthenticationFormUseCase { + func execute(uuid: String) async throws -> AuthenticationFormEntity +} diff --git a/Projects/Domain/AuthenticationDomain/Interface/UseCase/InputAuthenticationUseCase.swift b/Projects/Domain/AuthenticationDomain/Interface/UseCase/InputAuthenticationUseCase.swift new file mode 100644 index 00000000..b7f54041 --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Interface/UseCase/InputAuthenticationUseCase.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol InputAuthenticationUseCase { + func execute(uuid: String, req: InputAuthenticationRequestDTO) async throws +} diff --git a/Projects/Domain/AuthenticationDomain/Project.swift b/Projects/Domain/AuthenticationDomain/Project.swift new file mode 100644 index 00000000..ff9f11c7 --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Project.swift @@ -0,0 +1,12 @@ +import ProjectDescription +import ProjectDescriptionHelpers +import DependencyPlugin + +let project = Project.makeModule( + name: ModulePaths.Domain.AuthenticationDomain.rawValue, + product: .staticLibrary, + targets: [.interface, .testing], + internalDependencies: [ + .Domain.BaseDomain + ] +) diff --git a/Projects/Domain/AuthenticationDomain/Sources/DI/AuthenticationDomainComponent.swift b/Projects/Domain/AuthenticationDomain/Sources/DI/AuthenticationDomainComponent.swift new file mode 100644 index 00000000..5de122ee --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Sources/DI/AuthenticationDomainComponent.swift @@ -0,0 +1,23 @@ +import NeedleFoundation +import AuthenticationDomainInterface +import JwtStoreInterface + +public protocol AuthenticationDomainDependency: Dependency { + var jwtStoreBuildable: any JwtStoreBuildable { get } +} + +public final class AuthenticationDomainComponent: Component, AuthenticationDomainBuildable { + public var inputAuthenticationUseCase: any InputAuthenticationUseCase { + InputAuthenticationUseCaseImpl(authenticationRepository: authenticationRepository) + } + + public var fetchAuthenticationFormUseCase: any FetchAuthenticationFormUseCase { + FetchAuthenticationFormUseCaseImpl(authenticationRepository: authenticationRepository) + } + public var authenticationRepository: any AuthenticationRepository { + AuthenticationRepositoryImpl(remoteAuthenticationDataSource: remoteAuthenticationDataSource) + } + var remoteAuthenticationDataSource: any RemoteAuthenticationDataSource { + RemoteAuthenticationDataSourceImpl(jwtStore: dependency.jwtStoreBuildable.jwtStore) + } +} diff --git a/Projects/Domain/AuthenticationDomain/Sources/DTO/Response/ContentsResponseDTO.swift b/Projects/Domain/AuthenticationDomain/Sources/DTO/Response/ContentsResponseDTO.swift new file mode 100644 index 00000000..3eed8a99 --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Sources/DTO/Response/ContentsResponseDTO.swift @@ -0,0 +1,92 @@ +import Foundation +import AuthenticationDomainInterface + +public struct AuthenticationFormResponseDTO: Decodable { + public let files: [FilesResponseDTO] + public let contents: [ContentsResponseDTO] + + public init(files: [FilesResponseDTO], contents: [ContentsResponseDTO]) { + self.files = files + self.contents = contents + } +} + +public struct FilesResponseDTO: Decodable { + public let name: String + public let url: String +} + +public struct ContentsResponseDTO: Decodable { + public let title: String + public let sections: [SectionsResponseDTO] + + public struct SectionsResponseDTO: Decodable { + public let sectionId: String + public let sectionName: String + public let maxCount: Double + public let fields: [FieldsResponseDTO] + } + + public struct FieldsResponseDTO: Decodable { + public let fieldId: String + public let sectionType: FieldType + public let scoreDescription: String? + public let values: [ValuesResponseDTO]? + public let example: String? + } + + public struct ValuesResponseDTO: Decodable { + public let selectId: String + public let value: String + } +} + +extension AuthenticationFormResponseDTO { + func toDomain() -> AuthenticationFormEntity { + AuthenticationFormEntity( + files: files.map { $0.toDomain() }, + areas: contents.map { $0.toDomain() } + ) + } +} + +extension FilesResponseDTO { + func toDomain() -> AuthenticationFormEntity.File { + AuthenticationFormEntity.File(name: name, url: url) + } +} + +extension ContentsResponseDTO { + func toDomain() -> AuthenticationFormEntity.Area { + AuthenticationFormEntity.Area(title: title, sections: sections.map { $0.toDomain() }) + } +} + +extension ContentsResponseDTO.SectionsResponseDTO { + func toDomain() -> SectionEntity { + SectionEntity( + sectionId: sectionId, + title: sectionName, + maxCount: maxCount, + fields: fields.map { $0.toDomain() } + ) + } +} + +extension ContentsResponseDTO.FieldsResponseDTO { + func toDomain() -> FieldEntity { + FieldEntity( + fieldId: fieldId, + type: sectionType, + scoreDescription: scoreDescription, + values: values.map { $0.map { $0.toDomain() } }, + placeholder: example + ) + } +} + +extension ContentsResponseDTO.ValuesResponseDTO { + func toDomain() -> ValueEntity { + ValueEntity(selectId: selectId, value: value) + } +} diff --git a/Projects/Domain/AuthenticationDomain/Sources/DataSource/RemoteAuthenticationDataSourceImpl.swift b/Projects/Domain/AuthenticationDomain/Sources/DataSource/RemoteAuthenticationDataSourceImpl.swift new file mode 100644 index 00000000..a342c378 --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Sources/DataSource/RemoteAuthenticationDataSourceImpl.swift @@ -0,0 +1,12 @@ +import AuthenticationDomainInterface +import BaseDomain + +final class RemoteAuthenticationDataSourceImpl: BaseRemoteDataSource, RemoteAuthenticationDataSource { + func fetchAuthenticationForm(uuid: String) async throws -> AuthenticationFormEntity { + try await request(.fetchAuthenticationForm(uuid: uuid), dto: AuthenticationFormResponseDTO.self).toDomain() + } + + func inputAuthentication(uuid: String, req: InputAuthenticationRequestDTO) async throws { + try await request(.inputAuthentication(uuid: uuid, req: req)) + } +} diff --git a/Projects/Domain/AuthenticationDomain/Sources/Endpoint/AuthenticationEndpoint.swift b/Projects/Domain/AuthenticationDomain/Sources/Endpoint/AuthenticationEndpoint.swift new file mode 100644 index 00000000..a74ebacb --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Sources/Endpoint/AuthenticationEndpoint.swift @@ -0,0 +1,64 @@ +import AuthenticationDomainInterface +import BaseDomain +import Emdpoint + +enum AuthenticationEndpoint { + case fetchAuthenticationForm(uuid: String) + case inputAuthentication(uuid: String, req: InputAuthenticationRequestDTO) +} + +extension AuthenticationEndpoint: SMSEndpoint { + typealias ErrorType = AuthenticationDomainError + + var domain: SMSDomain { + .authentication + } + + var route: Route { + switch self { + case let .fetchAuthenticationForm(uuid): + return .get("/form/\(uuid)") + case let .inputAuthentication(uuid, _): + return .post("/submit/\(uuid)") + } + } + + var task: HTTPTask { + switch self { + case .fetchAuthenticationForm: + return .requestPlain + + case let .inputAuthentication(_, req): + return .requestJSONEncodable(req) + + default: + return .requestPlain + } + } + + var jwtTokenType: JwtTokenType { + switch self { + case .fetchAuthenticationForm: + return .accessToken + + case .inputAuthentication: + return .accessToken + + default: + return .none + } + } + + var errorMap: [Int: ErrorType]? { + switch self { + case .fetchAuthenticationForm: + return [ + 500: .internalServerError + ] + case .inputAuthentication: + return [ + 500: .internalServerError + ] + } + } +} diff --git a/Projects/Domain/AuthenticationDomain/Sources/Repository/AuthenticationRepositoryImpl.swift b/Projects/Domain/AuthenticationDomain/Sources/Repository/AuthenticationRepositoryImpl.swift new file mode 100644 index 00000000..58687e60 --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Sources/Repository/AuthenticationRepositoryImpl.swift @@ -0,0 +1,17 @@ +import AuthenticationDomainInterface + +struct AuthenticationRepositoryImpl: AuthenticationRepository { + private let remoteAuthenticationDataSource: any RemoteAuthenticationDataSource + + init(remoteAuthenticationDataSource: any RemoteAuthenticationDataSource) { + self.remoteAuthenticationDataSource = remoteAuthenticationDataSource + } + + func fetchAuthenticationForm(uuid: String) async throws -> AuthenticationFormEntity { + try await remoteAuthenticationDataSource.fetchAuthenticationForm(uuid: uuid) + } + + func inputAuthentication(uuid: String, req: InputAuthenticationRequestDTO) async throws { + try await remoteAuthenticationDataSource.inputAuthentication(uuid: uuid, req: req) + } +} diff --git a/Projects/Domain/AuthenticationDomain/Sources/UseCase/FetchAuthenticationFormUseCaseImpl.swift b/Projects/Domain/AuthenticationDomain/Sources/UseCase/FetchAuthenticationFormUseCaseImpl.swift new file mode 100644 index 00000000..bfccc14f --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Sources/UseCase/FetchAuthenticationFormUseCaseImpl.swift @@ -0,0 +1,13 @@ +import AuthenticationDomainInterface + +struct FetchAuthenticationFormUseCaseImpl: FetchAuthenticationFormUseCase { + private let authenticationRepository: any AuthenticationRepository + + init(authenticationRepository: any AuthenticationRepository) { + self.authenticationRepository = authenticationRepository + } + + func execute(uuid: String) async throws -> AuthenticationFormEntity { + try await authenticationRepository.fetchAuthenticationForm(uuid: uuid) + } +} diff --git a/Projects/Domain/AuthenticationDomain/Sources/UseCase/InputAuthenticationFormUseCaseImpl.swift b/Projects/Domain/AuthenticationDomain/Sources/UseCase/InputAuthenticationFormUseCaseImpl.swift new file mode 100644 index 00000000..3b3b898c --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Sources/UseCase/InputAuthenticationFormUseCaseImpl.swift @@ -0,0 +1,13 @@ +import AuthenticationDomainInterface + +struct InputAuthenticationUseCaseImpl: InputAuthenticationUseCase { + private let authenticationRepository: any AuthenticationRepository + + init(authenticationRepository: any AuthenticationRepository) { + self.authenticationRepository = authenticationRepository + } + + func execute(uuid: String, req: InputAuthenticationRequestDTO) async throws { + try await authenticationRepository.inputAuthentication(uuid: uuid, req: req) + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Interface/Interface.swift b/Projects/Domain/AuthenticationDomain/Testing/Testing.swift similarity index 100% rename from Projects/Feature/GSMAuthenticationFormFeature/Interface/Interface.swift rename to Projects/Domain/AuthenticationDomain/Testing/Testing.swift diff --git a/Projects/Domain/BaseDomain/Sources/Base/SMSDomain.swift b/Projects/Domain/BaseDomain/Sources/Base/SMSDomain.swift index e070454a..2b3c6de4 100644 --- a/Projects/Domain/BaseDomain/Sources/Base/SMSDomain.swift +++ b/Projects/Domain/BaseDomain/Sources/Base/SMSDomain.swift @@ -8,4 +8,5 @@ public enum SMSDomain: String { case techStack = "stack" case user case teacher + case authentication } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift index f2e6e25a..e3f83cea 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift @@ -8,53 +8,56 @@ struct GSMAuthenticationFormDemoApp: App { let uiModel = GSMAuthenticationFormUIModel( areas: [ .init( - title: "Area", + title: "전공 영역", sections: [ .init( - title: "Section1", - description: "Description", - currentFieldCount: 2, + title: "TOPCIT", + sectionId: "3ef0c164-430b-4afa-9785-8f5ca7b1decf", + currentFieldCount: 1, fields: [ .init( - key: "text", + fieldId: "e1b05d28-7caa-416a-9c6e-d6ad1b2bb887", type: .text(value: nil), - placeholder: "text placeholder" - ), - .init( - key: "number", - type: .number(value: nil), - placeholder: "number placeholder" - ), - .init( - key: "file", - type: .file(fileName: nil), - placeholder: "file placeholder" + scoreDescription: "취득 점수 * 3.3", + placeholder: "200점" ) ] ), .init( - title: "Section2", - description: "Description", + title: "교내 대회 및 교육 참가", + sectionId: "7d9bffc8-caa0-4b65-9465-7f79da375e48", currentFieldCount: 3, fields: [ .init( - key: "boolean", - type: .boolean(isSelcted: false), - placeholder: nil + fieldId: "3a73757b-35cb-45de-875b-78da8b41970c", + type: .file(fileName: nil), + scoreDescription: "GSM Festival, 교내해커톤 대회, 전공동아리 발표 대회", + placeholder: "1회당 50점" ), .init( - key: "select", - type: .select(selectedValue: nil, values: ["a", "b", "c"]), - placeholder: "select placeholder" + fieldId: "89990ad2-0ed1-4f0f-8e6d-fa33bb4e4b5b", + type: .file(fileName: nil), + scoreDescription: "전공특강(방과후)", + placeholder: "1회당 5점 최대 4회" + ), + .init( + fieldId: "d18a109a-377f-4392-ad32-4d3823c8974c", + type: .file(fileName: nil), + scoreDescription: "전공 관련 방과 후 학교 이수", + placeholder: "1회당 15점 최대 2회" ) ] ) ] ) ], - files: [.init(name: "내가 짱이다.pdf", url: "https://pdfobject.com/pdf/sample.pdf"), .init(name: "내가 짱인데요.pdf", url: "https://pdfobject.com/pdf/sample.pdf")] + files: [ + .init(name: "독후감 파일", url: "https://sms-bucket-104.s3.ap-northeast-2.amazonaws.com/sms-main-server/1.hwp"), + .init(name: "토익 점수표", url: "https://sms-bucket-104.s3.ap-northeast-2.amazonaws.com/sms-main-server/1A00E31F-7B57-4F79-86D3-E7EFAB7F408A_1_102_o.jpeg") + ] ) + GSMAuthenticationFormBuilderView(uiModel: uiModel) { _ in } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Interface/GSMAuthenticationBuildable.swift b/Projects/Feature/GSMAuthenticationFormFeature/Interface/GSMAuthenticationBuildable.swift new file mode 100644 index 00000000..de0975b5 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Interface/GSMAuthenticationBuildable.swift @@ -0,0 +1,6 @@ +import SwiftUI + +public protocol GSMAuthenticationBuildable { + associatedtype ViewType: View + func makeView() -> ViewType +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Interface/GSMAuthenticationDelegate.swift b/Projects/Feature/GSMAuthenticationFormFeature/Interface/GSMAuthenticationDelegate.swift new file mode 100644 index 00000000..2eb2bb3e --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Interface/GSMAuthenticationDelegate.swift @@ -0,0 +1,2 @@ +public protocol GSMAuthenticationDelegate: AnyObject { +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/DI/GSMAuthenticationComponent.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/DI/GSMAuthenticationComponent.swift new file mode 100644 index 00000000..4e140e80 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/DI/GSMAuthenticationComponent.swift @@ -0,0 +1,29 @@ +import BaseFeature +import Foundation +import NeedleFoundation +import SwiftUI +import GSMAuthenticationFormFeatureInterface +import AuthenticationDomainInterface + +public protocol GSMAuthenticationDependency: Dependency { + var authenticationDomainBuildable: any AuthenticationDomainBuildable { get } +} + +public final class GSMAuthenticationComponent: Component, GSMAuthenticationBuildable { + public func makeView() -> some View { + let model = GSMAuthenticationFormModel() + let intent = GSMAuthenticationFormIntent( + model: model, + fetchAuthenticationFormUseCase: dependency.authenticationDomainBuildable.fetchAuthenticationFormUseCase, + inputAuthenticationUseCase: dependency.authenticationDomainBuildable.inputAuthenticationUseCase + ) + let container = MVIContainer( + intent: intent as GSMAuthenticationFormIntentProtocol, + model: model as GSMAuthenticationFormStateProtocol, + modelChangePublisher: model.objectWillChange + ) + return GSMAuthenticationFormView( + container: container + ) + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift index 1c4069b4..76a33092 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift @@ -1,7 +1,148 @@ +import AuthenticationDomainInterface +import ConcurrencyUtil +import DateUtil + final class GSMAuthenticationFormIntent: GSMAuthenticationFormIntentProtocol { weak var model: (any GSMAuthenticationFormActionProtocol)? + private let fetchAuthenticationFormUseCase: any FetchAuthenticationFormUseCase + private let inputAuthenticationUseCase: any InputAuthenticationUseCase - init(model: any GSMAuthenticationFormActionProtocol) { + init( + model: any GSMAuthenticationFormActionProtocol, + fetchAuthenticationFormUseCase: any FetchAuthenticationFormUseCase, + inputAuthenticationUseCase: any InputAuthenticationUseCase + ) { self.model = model + self.fetchAuthenticationFormUseCase = fetchAuthenticationFormUseCase + self.inputAuthenticationUseCase = inputAuthenticationUseCase + } + + func onAppear() { + Task { + do { + let uiModel = try await fetchAuthenticationFormUseCase.execute( + uuid: "54030dd1-0f3b-498a-b644-747769dfdca2" + ) + + model?.updateGSMAuthenticationFormUIModel( + uiModel: .init( + areas: uiModel.areas.map { $0.toModel() }, + files: uiModel.files.map { $0.toModel() } + ) + ) + } + } + } + + func saveButtonDidTap(state: any GSMAuthenticationFormStateProtocol) { + Task { + do { + try await inputAuthenticationUseCase.execute( + uuid: "54030dd1-0f3b-498a-b644-747769dfdca2", + req: convertToDTO(model: state.uiModel) + ) + } + } + } + + func convertToDTO(model: GSMAuthenticationFormUIModel) -> InputAuthenticationRequestDTO { + var contents = [Content]() + + // 끄어어어억 + for area in model.areas { + for section in area.sections { + var objects = [Object]() + + for field in section.fields { + let sectionType: FieldType + let value: String + let selectID: String + + switch field.type { + case .text(let textValue): + sectionType = .text + value = textValue ?? "" + selectID = section.sectionId + case .number(let numberValue): + sectionType = .number + value = numberValue.map { String($0) } ?? "" + selectID = section.sectionId + case .boolean(let boolValue): + sectionType = .boolean + value = boolValue.map { String($0) } ?? "" + selectID = section.sectionId + case .file(let fileName): + sectionType = .file + value = fileName ?? "" + selectID = section.sectionId + case .select(let selectedValue, let values): + sectionType = .select + value = selectedValue ?? "" + selectID = values.first(where: { $0 == selectedValue }) ?? "" + } + + let object = Object( + fieldID: field.fieldId, + sectionType: sectionType, + value: value, + selectID: selectID + ) + + objects.append(object) + } + + let content = Content(sectionID: section.sectionId, objects: objects) + contents.append(content) + } + } + + return InputAuthenticationRequestDTO(contents: contents) + } +} + +extension AuthenticationFormEntity.File { + func toModel() -> GSMAuthenticationFormUIModel.File { + GSMAuthenticationFormUIModel.File.init(name: name, url: url) + } +} + +extension AuthenticationFormEntity.Area { + func toModel() -> GSMAuthenticationFormUIModel.Area { + GSMAuthenticationFormUIModel.Area.init( + title: title, + sections: sections.map { $0.toModel() } + ) + } +} + +extension SectionEntity { + func toModel() -> GSMAuthenticationFormUIModel.Area.Section { + GSMAuthenticationFormUIModel.Area.Section.init( + title: title, + sectionId: sectionId, + currentFieldCount: maxCount, + fields: fields.map { $0.toModel() } + ) + } +} + +extension FieldEntity { + func toModel() -> GSMAuthenticationFormUIModel.Area.Section.Field { + GSMAuthenticationFormUIModel.Area.Section.Field.init( + fieldId: fieldId, + type: transformType(type: type), + scoreDescription: scoreDescription, + placeholder: placeholder + ) + } + + func transformType(type: FieldType) -> GSMAuthenticationFormUIModel.Area.Section.Field.FieldType { + switch type { + case .text: .text(value: nil) + case .number: .number(value: nil) + case .boolean: .boolean(isSelcted: nil) + case .file: .file(fileName: nil) + case .select: .select(selectedValue: nil, values: []) + } } } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift index f58b3616..b7b99ee4 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift @@ -1,3 +1,6 @@ +import Foundation + protocol GSMAuthenticationFormIntentProtocol { - + func onAppear() + func saveButtonDidTap(state: any GSMAuthenticationFormStateProtocol) } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormFieldModel.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormFieldModel.swift new file mode 100644 index 00000000..1763d645 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormFieldModel.swift @@ -0,0 +1,10 @@ +struct GSMAuthenticationFormFieldModel { + let sectionId: String + let sectionType: GSMAuthenticationFormUIModel.Area.Section.Field.FieldType + let objects: [Object] + + struct Object { + let selectId: String + let value: GSMAuthenticationFormUIModel.Area.Section.Field.FieldType + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift index 948e1010..61482c38 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift @@ -1,7 +1,17 @@ -final class GSMAuthenticationFormModel: GSMAuthenticationFormStateProtocol { - +import Foundation +import FoundationUtil + +final class GSMAuthenticationFormModel: ObservableObject, GSMAuthenticationFormStateProtocol { + @Published var uiModel: GSMAuthenticationFormUIModel = .init(areas: [], files: []) + @Published var fieldContents: [GSMAuthenticationFormFieldModel] = [] } extension GSMAuthenticationFormModel: GSMAuthenticationFormActionProtocol { - + func updateGSMAuthenticationFormUIModel(uiModel: GSMAuthenticationFormUIModel) { + self.uiModel = uiModel + } + + func updateFieldContents(fields: [GSMAuthenticationFormFieldModel]) { + self.fieldContents = fields + } } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift index 1b20355c..90b6ca9b 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift @@ -1,8 +1,9 @@ - protocol GSMAuthenticationFormStateProtocol { - + var uiModel: GSMAuthenticationFormUIModel { get } + var fieldContents: [GSMAuthenticationFormFieldModel] { get } } protocol GSMAuthenticationFormActionProtocol: AnyObject { - + func updateGSMAuthenticationFormUIModel(uiModel: GSMAuthenticationFormUIModel) + func updateFieldContents(fields: [GSMAuthenticationFormFieldModel]) } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift index 0c77185a..cb23063b 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift @@ -11,13 +11,14 @@ struct GSMAuthenticationFormUIModel { struct Section { let title: String - let description: String - let currentFieldCount: Int + let sectionId: String + let currentFieldCount: Double let fields: [Field] struct Field { - let key: String + let fieldId: String let type: FieldType + let scoreDescription: String? let placeholder: String? enum FieldType { diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift index 8ec5eb9f..4ef71d7d 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift @@ -36,8 +36,6 @@ struct GSMAuthenticationFormBuilderView: View { ScrollView { GSMAuthenticationFileDownloadView(uiModel: uiModel.files) - SMSSeparator() - areaList(areas: uiModel.areas) } } @@ -46,6 +44,9 @@ struct GSMAuthenticationFormBuilderView: View { private func areaList(areas: [Area]) -> some View { LazyVStack(spacing: 16) { ForEach(uiModel.areas, id: \.title) { area in + SMSSeparator() + .padding(.bottom, 4) + HStack(spacing: 16) { SMSText(area.title, font: .title1) @@ -60,69 +61,76 @@ struct GSMAuthenticationFormBuilderView: View { .buttonWrapper { } } + .padding(.horizontal, 20) sectionList(sections: area.sections) + .padding(.horizontal, 20) } } - .padding(20) + } @ViewBuilder private func sectionList(sections: [Section]) -> some View { - LazyVStack(spacing: 16) { - ForEach(sections, id: \.title) { section in + LazyVStack(spacing: 0) { + ForEach(sections, id: \.sectionId) { section in VStack { - ForEach(0.. some View { + private func fieldList(key: String, fields: [Field]) -> some View { LazyVStack(spacing: 16) { - ForEach(fields, id: \.key) { field in - fieldView(field: field) + ForEach(fields, id: \.fieldId) { field in + fieldView(key: field.fieldId, field: field) + .titleWrapper( + field.scoreDescription ?? "", + position: .bottom(.leading), + font: .caption1, + color: .neutral(.n30) + ) } } } @ViewBuilder - private func fieldView(field: Field) -> some View { + private func fieldView(key: String, field: Field) -> some View { switch field.type { - case let .text(value): - textTypeFieldView(key: field.key, placeholder: field.placeholder, text: value) + case let .text(value): + textTypeFieldView(key: key, placeholder: field.placeholder, text: value) - case let .number(value): - numberTypeFieldView(key: field.key, placeholder: field.placeholder, number: value) + case let .number(value): + numberTypeFieldView(key: key, placeholder: field.placeholder, number: value) - case let .boolean(isSelected): - booleanTypeFieldView(key: field.key, isSelected: isSelected) + case let .boolean(isSelected): + booleanTypeFieldView(key: key, isSelected: isSelected) - case let .file(fileName): - fileTypeFieldView(key: field.key, placeholder: field.placeholder, fileName: fileName) + case let .file(fileName): + fileTypeFieldView(key: key, placeholder: field.placeholder, fileName: fileName) - case let .select(selectedValue, values): - selectTypeFieldView( - key: field.key, - placeholder: field.placeholder, - selectedValue: selectedValue, - values: values + case let .select(selectedValue, values): + selectTypeFieldView( + key: key, + placeholder: field.placeholder, + selectedValue: selectedValue, + values: values ) } } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift index a177ee0c..7f8317cc 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift @@ -1,7 +1,41 @@ +import BaseFeature import SwiftUI +import DesignSystem +import ViewUtil struct GSMAuthenticationFormView: View { + @Environment(\.dismiss) var dismiss + @Environment(\.safeAreaInsets) var safeAreaInsets + @StateObject var container: MVIContainer + var intent: any GSMAuthenticationFormIntentProtocol { container.intent } + var state: any GSMAuthenticationFormStateProtocol { container.model } + + init( + container: MVIContainer + ) { + self._container = StateObject(wrappedValue: container) + } + var body: some View { - EmptyView() + ZStack { + GSMAuthenticationFormBuilderView(uiModel: state.uiModel) { _ in + } + .padding(.bottom, safeAreaInsets.bottom + 16) + .onAppear { + intent.onAppear() + } + .overlay(alignment: .bottom) { + CTAButton(text: "저장") { + intent.saveButtonDidTap(state: state) + } + .frame(maxWidth: .infinity) + .padding(.horizontal, 20) + } + .navigationTitle("인증제") + .smsBackButton( + dismiss: dismiss + ) + .navigationBarTitleDisplayMode(.inline) + } } } diff --git a/Projects/Feature/MainFeature/Project.swift b/Projects/Feature/MainFeature/Project.swift index 4eb8bd6f..580b4c6e 100644 --- a/Projects/Feature/MainFeature/Project.swift +++ b/Projects/Feature/MainFeature/Project.swift @@ -11,6 +11,7 @@ let project = Project.makeModule( .Feature.StudentDetailFeatureInterface, .Feature.FilterFeatureInterface, .Feature.MyPageFeatureInterface, + .Feature.GSMAuthenticationFormFeatureInterface, .Domain.StudentDomainInterface, .Domain.UserDomainInterface ] diff --git a/Projects/Feature/MainFeature/Sources/DI/MainComponent.swift b/Projects/Feature/MainFeature/Sources/DI/MainComponent.swift index 3b867d29..8d0dc021 100644 --- a/Projects/Feature/MainFeature/Sources/DI/MainComponent.swift +++ b/Projects/Feature/MainFeature/Sources/DI/MainComponent.swift @@ -6,6 +6,7 @@ import StudentDetailFeatureInterface import StudentDomainInterface import FilterFeatureInterface import UserDomainInterface +import GSMAuthenticationFormFeatureInterface import MyPageFeatureInterface public protocol MainDependency: Dependency { @@ -13,6 +14,7 @@ public protocol MainDependency: Dependency { var filterBuildable: any FilterBuildable { get } var myPageBuildable: any MyPageBuildable { get } var studentDetailBuildable: any StudentDetailBuildable { get } + var gsmAuthenticationBuildable: any GSMAuthenticationBuildable { get } var userDomainBuildable: any UserDomainBuildable { get } } @@ -34,7 +36,8 @@ public final class MainComponent: Component, MainBuildable { container: container, studentDetailBuildable: dependency.studentDetailBuildable, filterBuildable: dependency.filterBuildable, - myPageBuildable: dependency.myPageBuildable + myPageBuildable: dependency.myPageBuildable, + gsmAuthenticatoinBuildable: dependency.gsmAuthenticationBuildable ) } } diff --git a/Projects/Feature/MainFeature/Sources/Intent/MainIntentProtocol.swift b/Projects/Feature/MainFeature/Sources/Intent/MainIntentProtocol.swift index 5dfcaeb6..29011df7 100644 --- a/Projects/Feature/MainFeature/Sources/Intent/MainIntentProtocol.swift +++ b/Projects/Feature/MainFeature/Sources/Intent/MainIntentProtocol.swift @@ -2,8 +2,9 @@ import Foundation import FilterFeatureInterface import MyPageFeatureInterface import StudentDomainInterface +import GSMAuthenticationFormFeatureInterface -protocol MainIntentProtocol: FilterDelegate, MyPageDelegate { +protocol MainIntentProtocol: FilterDelegate, MyPageDelegate, GSMAuthenticationDelegate { func reachedBottom(page: Int, isLast: Bool, filterOption: FilterOption?) func refresh(filterOption: FilterOption?) func filterIsRequired() diff --git a/Projects/Feature/MainFeature/Sources/Scene/MainView.swift b/Projects/Feature/MainFeature/Sources/Scene/MainView.swift index d2145014..ac2c2f47 100644 --- a/Projects/Feature/MainFeature/Sources/Scene/MainView.swift +++ b/Projects/Feature/MainFeature/Sources/Scene/MainView.swift @@ -8,6 +8,7 @@ import UserDomainInterface import ViewUtil import FilterFeatureInterface import MyPageFeatureInterface +import GSMAuthenticationFormFeatureInterface enum MainStudentIDProperty { static let studentScrollToTopID = "STUDENT_SCROLL_TO_TOP" @@ -21,17 +22,20 @@ struct MainView: View { private let studentDetailBuildable: any StudentDetailBuildable private let filterBuildable: any FilterBuildable private let myPageBuildable: any MyPageBuildable + private let gsmAuthenticatoinBuildable: any GSMAuthenticationBuildable init( container: MVIContainer, studentDetailBuildable: any StudentDetailBuildable, filterBuildable: any FilterBuildable, - myPageBuildable: any MyPageBuildable + myPageBuildable: any MyPageBuildable, + gsmAuthenticatoinBuildable: any GSMAuthenticationBuildable ) { self._container = StateObject(wrappedValue: container) self.studentDetailBuildable = studentDetailBuildable self.filterBuildable = filterBuildable self.myPageBuildable = myPageBuildable + self.gsmAuthenticatoinBuildable = gsmAuthenticatoinBuildable } var body: some View { @@ -116,7 +120,7 @@ struct MainView: View { ) ) .navigate( - to: myPageBuildable.makeView(delegate: intent).eraseToAnyView(), + to: gsmAuthenticatoinBuildable.makeView().eraseToAnyView(), when: Binding( get: { state.isPresentedMyPage }, set: { _ in intent.myPageDismissed() } From 9ae3a2bee6c00359198b6d37b95f86f39b29fec5 Mon Sep 17 00:00:00 2001 From: Sunghun Kim <81547954+kimsh153@users.noreply.github.com> Date: Fri, 5 Jul 2024 01:16:11 +0900 Subject: [PATCH 02/13] :recycle: :: GSMAuthentication refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 흑흑 힘들어요 --- .../Briefcases.imageset/Briefcases.png | Bin 0 -> 412 bytes .../Briefcases.imageset/Contents.json | 12 + .../Person.imageset/Contents.json | 12 + .../Icons.xcassets/Person.imageset/person.svg | 4 + .../DesignSystem/Sources/Icon/SMSIcon.swift | 8 + .../InputAuthenticationRequestDTO.swift | 18 +- .../RemoteAuthenticationDataSource.swift | 4 +- .../Entity/AuthenticationFormEntity.swift | 52 ++++ .../Interface/Entity/FieldEntity.swift | 15 - .../Interface/Entity/SectionEntity.swift | 13 - .../Interface/Entity/ValueEntity.swift | 9 - .../Interface/Enum/FieldEnum.swift | 1 - .../Repository/AuthenticationRepository.swift | 4 +- .../FetchAuthenticationFormUseCase.swift | 2 +- .../UseCase/InputAuthenticationUseCase.swift | 2 +- .../DTO/Response/ContentsResponseDTO.swift | 22 +- .../RemoteAuthenticationDataSourceImpl.swift | 8 +- .../Endpoint/AuthenticationEndpoint.swift | 14 +- .../AuthenticationRepositoryImpl.swift | 8 +- .../FetchAuthenticationFormUseCaseImpl.swift | 4 +- .../InputAuthenticationFormUseCaseImpl.swift | 4 +- .../Demo/Sources/AppDelegate.swift | 93 ++++--- .../Intent/GSMAuthenticationFormIntent.swift | 156 +++++++---- .../GSMAuthenticationFormIntentProtocol.swift | 46 ++++ .../GSMAuthenticationFormFieldModel.swift | 4 +- .../Model/GSMAuthenticationFormModel.swift | 43 +++ .../GSMAuthenticationFormModelProtocol.swift | 52 +++- .../Model/GSMAuthenticationFormUIModel.swift | 61 +++-- .../GSMAuthenticationFormBuilderView.swift | 258 ++++++++++++++---- .../Scene/GSMAuthenticationFormView.swift | 72 ++++- .../Sources/Intent/MainIntent.swift | 16 ++ .../Sources/Intent/MainIntentProtocol.swift | 4 + .../MainFeature/Sources/Model/MainModel.swift | 10 + .../Sources/Model/MainModelProtocol.swift | 4 + .../MainFeature/Sources/Scene/MainView.swift | 50 +++- 35 files changed, 828 insertions(+), 257 deletions(-) create mode 100644 Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Briefcases.imageset/Briefcases.png create mode 100644 Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Briefcases.imageset/Contents.json create mode 100644 Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Person.imageset/Contents.json create mode 100644 Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Person.imageset/person.svg delete mode 100644 Projects/Domain/AuthenticationDomain/Interface/Entity/FieldEntity.swift delete mode 100644 Projects/Domain/AuthenticationDomain/Interface/Entity/SectionEntity.swift delete mode 100644 Projects/Domain/AuthenticationDomain/Interface/Entity/ValueEntity.swift diff --git a/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Briefcases.imageset/Briefcases.png b/Projects/Core/DesignSystem/Resources/Icon/Icons.xcassets/Briefcases.imageset/Briefcases.png new file mode 100644 index 0000000000000000000000000000000000000000..4492ad94ded7d319c430d8d6dfecb7478ccb1966 GIT binary patch literal 412 zcmV;N0b~A&P)cDH(~2bgUYu%oO?SOOtD zN!Kpz_r72Etp&~;8q2gG2>QJo`H&A=g{j1X6-Lo`QWoEU0WKbD#0CDJaVZxtpST2TDf(#_hMl=rqula0000 + + + diff --git a/Projects/Core/DesignSystem/Sources/Icon/SMSIcon.swift b/Projects/Core/DesignSystem/Sources/Icon/SMSIcon.swift index eea5bdbc..a2748043 100644 --- a/Projects/Core/DesignSystem/Sources/Icon/SMSIcon.swift +++ b/Projects/Core/DesignSystem/Sources/Icon/SMSIcon.swift @@ -20,6 +20,7 @@ public struct SMSIcon: View { public enum Icon: CaseIterable { case book + case briefcases case calendar case camera case check @@ -31,6 +32,7 @@ public struct SMSIcon: View { case greenCheck case photo case plus + case person case profile case profileSmallPlus case redPerson @@ -61,6 +63,9 @@ public struct SMSIcon: View { case .book: return DesignSystemAsset.Icons.book.swiftUIImage + case .briefcases: + return DesignSystemAsset.Icons.briefcases.swiftUIImage + case .calendar: return DesignSystemAsset.Icons.calendar.swiftUIImage @@ -94,6 +99,9 @@ public struct SMSIcon: View { case .plus: return DesignSystemAsset.Icons.plus.swiftUIImage + case .person: + return DesignSystemAsset.Icons.person.swiftUIImage + case .profile: return DesignSystemAsset.Icons.profile.swiftUIImage diff --git a/Projects/Domain/AuthenticationDomain/Interface/DTO/Request/InputAuthenticationRequestDTO.swift b/Projects/Domain/AuthenticationDomain/Interface/DTO/Request/InputAuthenticationRequestDTO.swift index 0d64ca9a..3b981222 100644 --- a/Projects/Domain/AuthenticationDomain/Interface/DTO/Request/InputAuthenticationRequestDTO.swift +++ b/Projects/Domain/AuthenticationDomain/Interface/DTO/Request/InputAuthenticationRequestDTO.swift @@ -18,16 +18,26 @@ public struct Content: Encodable { } } -// MARK: - Object public struct Object: Encodable { + public let groupId: String + public let fields: [Field] + + public init(groupId: String, fields: [Field]) { + self.groupId = groupId + self.fields = fields + } +} + +// MARK: - Object +public struct Field: Encodable { public let fieldID: String - public let sectionType: FieldType + public let fieldType: FieldType public let value: String public let selectID: String - public init(fieldID: String, sectionType: FieldType, value: String, selectID: String) { + public init(fieldID: String, fieldType: FieldType, value: String, selectID: String) { self.fieldID = fieldID - self.sectionType = sectionType + self.fieldType = fieldType self.value = value self.selectID = selectID } diff --git a/Projects/Domain/AuthenticationDomain/Interface/DataSource/RemoteAuthenticationDataSource.swift b/Projects/Domain/AuthenticationDomain/Interface/DataSource/RemoteAuthenticationDataSource.swift index 97bfb08a..3c62b8d1 100644 --- a/Projects/Domain/AuthenticationDomain/Interface/DataSource/RemoteAuthenticationDataSource.swift +++ b/Projects/Domain/AuthenticationDomain/Interface/DataSource/RemoteAuthenticationDataSource.swift @@ -1,7 +1,7 @@ import Foundation public protocol RemoteAuthenticationDataSource { - func fetchAuthenticationForm(uuid: String) async throws -> AuthenticationFormEntity + func fetchAuthenticationForm() async throws -> AuthenticationFormEntity - func inputAuthentication(uuid: String, req: InputAuthenticationRequestDTO) async throws + func inputAuthentication(req: InputAuthenticationRequestDTO) async throws } diff --git a/Projects/Domain/AuthenticationDomain/Interface/Entity/AuthenticationFormEntity.swift b/Projects/Domain/AuthenticationDomain/Interface/Entity/AuthenticationFormEntity.swift index 9eb5aaeb..c6e70016 100644 --- a/Projects/Domain/AuthenticationDomain/Interface/Entity/AuthenticationFormEntity.swift +++ b/Projects/Domain/AuthenticationDomain/Interface/Entity/AuthenticationFormEntity.swift @@ -29,3 +29,55 @@ extension AuthenticationFormEntity { } } } + +public struct SectionEntity { + public let sectionId: String + public let title: String + public let maxCount: Int + public let groups: [GroupEntity] + + public init(sectionId: String, title: String, maxCount: Int, groups: [GroupEntity]) { + self.sectionId = sectionId + self.title = title + self.maxCount = maxCount + self.groups = groups + } +} + +public struct GroupEntity { + public let groupId: String + public let maxScore: Double + public let fields: [FieldEntity] + + public init(groupId: String, maxScore: Double, fields: [FieldEntity]) { + self.groupId = groupId + self.maxScore = maxScore + self.fields = fields + } +} + +public struct FieldEntity { + public let fieldId: String + public let type: FieldType + public let scoreDescription: String? + public let values: [ValueEntity]? + public let placeholder: String? + + public init(fieldId: String, type: FieldType, scoreDescription: String?, values: [ValueEntity]?, placeholder: String?) { + self.fieldId = fieldId + self.type = type + self.scoreDescription = scoreDescription + self.values = values + self.placeholder = placeholder + } +} + +public struct ValueEntity { + public let selectId: String + public let value: String + + public init(selectId: String, value: String) { + self.selectId = selectId + self.value = value + } +} diff --git a/Projects/Domain/AuthenticationDomain/Interface/Entity/FieldEntity.swift b/Projects/Domain/AuthenticationDomain/Interface/Entity/FieldEntity.swift deleted file mode 100644 index e756d7c4..00000000 --- a/Projects/Domain/AuthenticationDomain/Interface/Entity/FieldEntity.swift +++ /dev/null @@ -1,15 +0,0 @@ -public struct FieldEntity { - public let fieldId: String - public let type: FieldType - public let scoreDescription: String? - public let values: [ValueEntity]? - public let placeholder: String? - - public init(fieldId: String, type: FieldType, scoreDescription: String?, values: [ValueEntity]?, placeholder: String?) { - self.fieldId = fieldId - self.type = type - self.scoreDescription = scoreDescription - self.values = values - self.placeholder = placeholder - } -} diff --git a/Projects/Domain/AuthenticationDomain/Interface/Entity/SectionEntity.swift b/Projects/Domain/AuthenticationDomain/Interface/Entity/SectionEntity.swift deleted file mode 100644 index be1c4c3a..00000000 --- a/Projects/Domain/AuthenticationDomain/Interface/Entity/SectionEntity.swift +++ /dev/null @@ -1,13 +0,0 @@ -public struct SectionEntity { - public let sectionId: String - public let title: String - public let maxCount: Double - public let fields: [FieldEntity] - - public init(sectionId: String, title: String, maxCount: Double, fields: [FieldEntity]) { - self.sectionId = sectionId - self.title = title - self.maxCount = maxCount - self.fields = fields - } -} diff --git a/Projects/Domain/AuthenticationDomain/Interface/Entity/ValueEntity.swift b/Projects/Domain/AuthenticationDomain/Interface/Entity/ValueEntity.swift deleted file mode 100644 index 913d378f..00000000 --- a/Projects/Domain/AuthenticationDomain/Interface/Entity/ValueEntity.swift +++ /dev/null @@ -1,9 +0,0 @@ -public struct ValueEntity { - public let selectId: String - public let value: String - - public init(selectId: String, value: String) { - self.selectId = selectId - self.value = value - } -} diff --git a/Projects/Domain/AuthenticationDomain/Interface/Enum/FieldEnum.swift b/Projects/Domain/AuthenticationDomain/Interface/Enum/FieldEnum.swift index 8a6f6dbf..7aec808c 100644 --- a/Projects/Domain/AuthenticationDomain/Interface/Enum/FieldEnum.swift +++ b/Projects/Domain/AuthenticationDomain/Interface/Enum/FieldEnum.swift @@ -4,5 +4,4 @@ public enum FieldType: String, Codable { case boolean = "BOOLEAN" case file = "FILE" case select = "SELECT" -// case selectValue = "SELECT_VALUE" } diff --git a/Projects/Domain/AuthenticationDomain/Interface/Repository/AuthenticationRepository.swift b/Projects/Domain/AuthenticationDomain/Interface/Repository/AuthenticationRepository.swift index f7ccc4d3..93ca19f6 100644 --- a/Projects/Domain/AuthenticationDomain/Interface/Repository/AuthenticationRepository.swift +++ b/Projects/Domain/AuthenticationDomain/Interface/Repository/AuthenticationRepository.swift @@ -1,6 +1,6 @@ import Foundation public protocol AuthenticationRepository { - func fetchAuthenticationForm(uuid: String) async throws -> AuthenticationFormEntity - func inputAuthentication(uuid: String, req: InputAuthenticationRequestDTO) async throws + func fetchAuthenticationForm() async throws -> AuthenticationFormEntity + func inputAuthentication(req: InputAuthenticationRequestDTO) async throws } diff --git a/Projects/Domain/AuthenticationDomain/Interface/UseCase/FetchAuthenticationFormUseCase.swift b/Projects/Domain/AuthenticationDomain/Interface/UseCase/FetchAuthenticationFormUseCase.swift index c2929896..22593e48 100644 --- a/Projects/Domain/AuthenticationDomain/Interface/UseCase/FetchAuthenticationFormUseCase.swift +++ b/Projects/Domain/AuthenticationDomain/Interface/UseCase/FetchAuthenticationFormUseCase.swift @@ -1,5 +1,5 @@ import Foundation public protocol FetchAuthenticationFormUseCase { - func execute(uuid: String) async throws -> AuthenticationFormEntity + func execute() async throws -> AuthenticationFormEntity } diff --git a/Projects/Domain/AuthenticationDomain/Interface/UseCase/InputAuthenticationUseCase.swift b/Projects/Domain/AuthenticationDomain/Interface/UseCase/InputAuthenticationUseCase.swift index b7f54041..c35c176d 100644 --- a/Projects/Domain/AuthenticationDomain/Interface/UseCase/InputAuthenticationUseCase.swift +++ b/Projects/Domain/AuthenticationDomain/Interface/UseCase/InputAuthenticationUseCase.swift @@ -1,5 +1,5 @@ import Foundation public protocol InputAuthenticationUseCase { - func execute(uuid: String, req: InputAuthenticationRequestDTO) async throws + func execute(req: InputAuthenticationRequestDTO) async throws } diff --git a/Projects/Domain/AuthenticationDomain/Sources/DTO/Response/ContentsResponseDTO.swift b/Projects/Domain/AuthenticationDomain/Sources/DTO/Response/ContentsResponseDTO.swift index 3eed8a99..1ba8dfd5 100644 --- a/Projects/Domain/AuthenticationDomain/Sources/DTO/Response/ContentsResponseDTO.swift +++ b/Projects/Domain/AuthenticationDomain/Sources/DTO/Response/ContentsResponseDTO.swift @@ -23,13 +23,19 @@ public struct ContentsResponseDTO: Decodable { public struct SectionsResponseDTO: Decodable { public let sectionId: String public let sectionName: String - public let maxCount: Double + public let maxCount: Int + public let groups: [GroupsResponseDTO] + } + + public struct GroupsResponseDTO: Decodable { + public let groupId: String + public let maxScore: Double public let fields: [FieldsResponseDTO] } public struct FieldsResponseDTO: Decodable { public let fieldId: String - public let sectionType: FieldType + public let fieldType: FieldType public let scoreDescription: String? public let values: [ValuesResponseDTO]? public let example: String? @@ -68,6 +74,16 @@ extension ContentsResponseDTO.SectionsResponseDTO { sectionId: sectionId, title: sectionName, maxCount: maxCount, + groups: groups.map { $0.toDomain() } + ) + } +} + +extension ContentsResponseDTO.GroupsResponseDTO { + func toDomain() -> GroupEntity { + GroupEntity( + groupId: groupId, + maxScore: maxScore, fields: fields.map { $0.toDomain() } ) } @@ -77,7 +93,7 @@ extension ContentsResponseDTO.FieldsResponseDTO { func toDomain() -> FieldEntity { FieldEntity( fieldId: fieldId, - type: sectionType, + type: fieldType, scoreDescription: scoreDescription, values: values.map { $0.map { $0.toDomain() } }, placeholder: example diff --git a/Projects/Domain/AuthenticationDomain/Sources/DataSource/RemoteAuthenticationDataSourceImpl.swift b/Projects/Domain/AuthenticationDomain/Sources/DataSource/RemoteAuthenticationDataSourceImpl.swift index a342c378..47bed4fd 100644 --- a/Projects/Domain/AuthenticationDomain/Sources/DataSource/RemoteAuthenticationDataSourceImpl.swift +++ b/Projects/Domain/AuthenticationDomain/Sources/DataSource/RemoteAuthenticationDataSourceImpl.swift @@ -2,11 +2,11 @@ import AuthenticationDomainInterface import BaseDomain final class RemoteAuthenticationDataSourceImpl: BaseRemoteDataSource, RemoteAuthenticationDataSource { - func fetchAuthenticationForm(uuid: String) async throws -> AuthenticationFormEntity { - try await request(.fetchAuthenticationForm(uuid: uuid), dto: AuthenticationFormResponseDTO.self).toDomain() + func fetchAuthenticationForm() async throws -> AuthenticationFormEntity { + try await request(.fetchAuthenticationForm, dto: AuthenticationFormResponseDTO.self).toDomain() } - func inputAuthentication(uuid: String, req: InputAuthenticationRequestDTO) async throws { - try await request(.inputAuthentication(uuid: uuid, req: req)) + func inputAuthentication(req: InputAuthenticationRequestDTO) async throws { + try await request(.inputAuthentication(req: req)) } } diff --git a/Projects/Domain/AuthenticationDomain/Sources/Endpoint/AuthenticationEndpoint.swift b/Projects/Domain/AuthenticationDomain/Sources/Endpoint/AuthenticationEndpoint.swift index a74ebacb..8519c27e 100644 --- a/Projects/Domain/AuthenticationDomain/Sources/Endpoint/AuthenticationEndpoint.swift +++ b/Projects/Domain/AuthenticationDomain/Sources/Endpoint/AuthenticationEndpoint.swift @@ -3,8 +3,8 @@ import BaseDomain import Emdpoint enum AuthenticationEndpoint { - case fetchAuthenticationForm(uuid: String) - case inputAuthentication(uuid: String, req: InputAuthenticationRequestDTO) + case fetchAuthenticationForm + case inputAuthentication(req: InputAuthenticationRequestDTO) } extension AuthenticationEndpoint: SMSEndpoint { @@ -16,10 +16,10 @@ extension AuthenticationEndpoint: SMSEndpoint { var route: Route { switch self { - case let .fetchAuthenticationForm(uuid): - return .get("/form/\(uuid)") - case let .inputAuthentication(uuid, _): - return .post("/submit/\(uuid)") + case .fetchAuthenticationForm: + return .get("/form") + case .inputAuthentication: + return .post("/submit") } } @@ -28,7 +28,7 @@ extension AuthenticationEndpoint: SMSEndpoint { case .fetchAuthenticationForm: return .requestPlain - case let .inputAuthentication(_, req): + case let .inputAuthentication(req): return .requestJSONEncodable(req) default: diff --git a/Projects/Domain/AuthenticationDomain/Sources/Repository/AuthenticationRepositoryImpl.swift b/Projects/Domain/AuthenticationDomain/Sources/Repository/AuthenticationRepositoryImpl.swift index 58687e60..d32410cc 100644 --- a/Projects/Domain/AuthenticationDomain/Sources/Repository/AuthenticationRepositoryImpl.swift +++ b/Projects/Domain/AuthenticationDomain/Sources/Repository/AuthenticationRepositoryImpl.swift @@ -7,11 +7,11 @@ struct AuthenticationRepositoryImpl: AuthenticationRepository { self.remoteAuthenticationDataSource = remoteAuthenticationDataSource } - func fetchAuthenticationForm(uuid: String) async throws -> AuthenticationFormEntity { - try await remoteAuthenticationDataSource.fetchAuthenticationForm(uuid: uuid) + func fetchAuthenticationForm() async throws -> AuthenticationFormEntity { + try await remoteAuthenticationDataSource.fetchAuthenticationForm() } - func inputAuthentication(uuid: String, req: InputAuthenticationRequestDTO) async throws { - try await remoteAuthenticationDataSource.inputAuthentication(uuid: uuid, req: req) + func inputAuthentication(req: InputAuthenticationRequestDTO) async throws { + try await remoteAuthenticationDataSource.inputAuthentication(req: req) } } diff --git a/Projects/Domain/AuthenticationDomain/Sources/UseCase/FetchAuthenticationFormUseCaseImpl.swift b/Projects/Domain/AuthenticationDomain/Sources/UseCase/FetchAuthenticationFormUseCaseImpl.swift index bfccc14f..2907d665 100644 --- a/Projects/Domain/AuthenticationDomain/Sources/UseCase/FetchAuthenticationFormUseCaseImpl.swift +++ b/Projects/Domain/AuthenticationDomain/Sources/UseCase/FetchAuthenticationFormUseCaseImpl.swift @@ -7,7 +7,7 @@ struct FetchAuthenticationFormUseCaseImpl: FetchAuthenticationFormUseCase { self.authenticationRepository = authenticationRepository } - func execute(uuid: String) async throws -> AuthenticationFormEntity { - try await authenticationRepository.fetchAuthenticationForm(uuid: uuid) + func execute() async throws -> AuthenticationFormEntity { + try await authenticationRepository.fetchAuthenticationForm() } } diff --git a/Projects/Domain/AuthenticationDomain/Sources/UseCase/InputAuthenticationFormUseCaseImpl.swift b/Projects/Domain/AuthenticationDomain/Sources/UseCase/InputAuthenticationFormUseCaseImpl.swift index 3b3b898c..23f6983c 100644 --- a/Projects/Domain/AuthenticationDomain/Sources/UseCase/InputAuthenticationFormUseCaseImpl.swift +++ b/Projects/Domain/AuthenticationDomain/Sources/UseCase/InputAuthenticationFormUseCaseImpl.swift @@ -7,7 +7,7 @@ struct InputAuthenticationUseCaseImpl: InputAuthenticationUseCase { self.authenticationRepository = authenticationRepository } - func execute(uuid: String, req: InputAuthenticationRequestDTO) async throws { - try await authenticationRepository.inputAuthentication(uuid: uuid, req: req) + func execute(req: InputAuthenticationRequestDTO) async throws { + try await authenticationRepository.inputAuthentication(req: req) } } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift index e3f83cea..0012d34d 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift @@ -7,44 +7,56 @@ struct GSMAuthenticationFormDemoApp: App { WindowGroup { let uiModel = GSMAuthenticationFormUIModel( areas: [ - .init( - title: "전공 영역", + GSMAuthenticationFormUIModel.Area( + title: "Area 1", sections: [ - .init( - title: "TOPCIT", - sectionId: "3ef0c164-430b-4afa-9785-8f5ca7b1decf", - currentFieldCount: 1, - fields: [ - .init( - fieldId: "e1b05d28-7caa-416a-9c6e-d6ad1b2bb887", - type: .text(value: nil), - scoreDescription: "취득 점수 * 3.3", - placeholder: "200점" - ) - ] - ), - .init( - title: "교내 대회 및 교육 참가", - sectionId: "7d9bffc8-caa0-4b65-9465-7f79da375e48", + GSMAuthenticationFormUIModel.Area.Section( + title: "Section 1", + sectionId: "sec1", + maxCount: 5, currentFieldCount: 3, - fields: [ - .init( - fieldId: "3a73757b-35cb-45de-875b-78da8b41970c", - type: .file(fileName: nil), - scoreDescription: "GSM Festival, 교내해커톤 대회, 전공동아리 발표 대회", - placeholder: "1회당 50점" - ), - .init( - fieldId: "89990ad2-0ed1-4f0f-8e6d-fa33bb4e4b5b", - type: .file(fileName: nil), - scoreDescription: "전공특강(방과후)", - placeholder: "1회당 5점 최대 4회" + groups: [ + GSMAuthenticationFormUIModel.Area.Section.Group( + groupId: "group1", + maxScore: 100.0, + fields: [ + GSMAuthenticationFormUIModel.Area.Section.Group.Field( + fieldId: "field1", + type: .text(value: "Sample text"), + scoreDescription: "Description 1", + placeholder: "Enter text" + ), + GSMAuthenticationFormUIModel.Area.Section.Group.Field( + fieldId: "field2", + type: .number(value: 42), + scoreDescription: "Description 2", + placeholder: "Enter number" + ), + GSMAuthenticationFormUIModel.Area.Section.Group.Field( + fieldId: "field3", + type: .boolean(selectedValue: "Yes", values: ["Yes", "No"]), + scoreDescription: "Description 3", + placeholder: "Select Yes/No" + ) + ] ), - .init( - fieldId: "d18a109a-377f-4392-ad32-4d3823c8974c", - type: .file(fileName: nil), - scoreDescription: "전공 관련 방과 후 학교 이수", - placeholder: "1회당 15점 최대 2회" + GSMAuthenticationFormUIModel.Area.Section.Group( + groupId: "group2", + maxScore: 80.0, + fields: [ + GSMAuthenticationFormUIModel.Area.Section.Group.Field( + fieldId: "field4", + type: .file(fileName: "document.pdf"), + scoreDescription: "Description 4", + placeholder: "Upload file" + ), + GSMAuthenticationFormUIModel.Area.Section.Group.Field( + fieldId: "field5", + type: .select(selectedValue: "Option 1", values: ["Option 1", "Option 2", "Option 3"]), + scoreDescription: "Description 5", + placeholder: "Select option" + ) + ] ) ] ) @@ -52,15 +64,14 @@ struct GSMAuthenticationFormDemoApp: App { ) ], files: [ - .init(name: "독후감 파일", url: "https://sms-bucket-104.s3.ap-northeast-2.amazonaws.com/sms-main-server/1.hwp"), - .init(name: "토익 점수표", url: "https://sms-bucket-104.s3.ap-northeast-2.amazonaws.com/sms-main-server/1A00E31F-7B57-4F79-86D3-E7EFAB7F408A_1_102_o.jpeg") + GSMAuthenticationFormUIModel.File(name: "File 1", url: "http://example.com/file1"), + GSMAuthenticationFormUIModel.File(name: "File 2", url: "http://example.com/file2") ] ) - - GSMAuthenticationFormBuilderView(uiModel: uiModel) { _ in - - } +// GSMAuthenticationFormBuilderView(intent: , uiModel: uiModel) { _ in +// +// } } } } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift index 76a33092..8970d54a 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift @@ -1,4 +1,5 @@ import AuthenticationDomainInterface +import Foundation import ConcurrencyUtil import DateUtil @@ -17,12 +18,28 @@ final class GSMAuthenticationFormIntent: GSMAuthenticationFormIntentProtocol { self.inputAuthenticationUseCase = inputAuthenticationUseCase } + func appendField( + area: Int, + sectionIndex: Int, + groupIndex: Int, + fields: [GSMAuthenticationFormUIModel.Area.Section.Group.Field] + ) { + model?.appendField( + area: area, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fields: fields + ) + } + + func deleteField(area: Int, sectionIndex: Int, groupIndex: Int) { + model?.deleteField(area: area, sectionIndex: sectionIndex, groupIndex: groupIndex) + } + func onAppear() { Task { do { - let uiModel = try await fetchAuthenticationFormUseCase.execute( - uuid: "54030dd1-0f3b-498a-b644-747769dfdca2" - ) + let uiModel = try await fetchAuthenticationFormUseCase.execute() model?.updateGSMAuthenticationFormUIModel( uiModel: .init( @@ -38,7 +55,6 @@ final class GSMAuthenticationFormIntent: GSMAuthenticationFormIntentProtocol { Task { do { try await inputAuthenticationUseCase.execute( - uuid: "54030dd1-0f3b-498a-b644-747769dfdca2", req: convertToDTO(model: state.uiModel) ) } @@ -46,57 +62,66 @@ final class GSMAuthenticationFormIntent: GSMAuthenticationFormIntentProtocol { } func convertToDTO(model: GSMAuthenticationFormUIModel) -> InputAuthenticationRequestDTO { - var contents = [Content]() - - // 끄어어어억 - for area in model.areas { - for section in area.sections { - var objects = [Object]() - - for field in section.fields { - let sectionType: FieldType - let value: String - let selectID: String - - switch field.type { - case .text(let textValue): - sectionType = .text - value = textValue ?? "" - selectID = section.sectionId - case .number(let numberValue): - sectionType = .number - value = numberValue.map { String($0) } ?? "" - selectID = section.sectionId - case .boolean(let boolValue): - sectionType = .boolean - value = boolValue.map { String($0) } ?? "" - selectID = section.sectionId - case .file(let fileName): - sectionType = .file - value = fileName ?? "" - selectID = section.sectionId - case .select(let selectedValue, let values): - sectionType = .select - value = selectedValue ?? "" - selectID = values.first(where: { $0 == selectedValue }) ?? "" + let contents: [Content] = model.areas.flatMap { area in + area.sections.map { section in + let objects: [Object] = section.groups.map { group in + let fields: [Field] = group.fields.map { field in + let fieldType: FieldType + let value: String + let selectID: String + + switch field.type { + case .text(let textValue): + fieldType = .text + value = textValue ?? "" + selectID = "" + case .number(let numberValue): + fieldType = .number + value = numberValue.map { String($0) } ?? "" + selectID = "" + case .boolean(let selectedValue, let values): + fieldType = .boolean + value = selectedValue ?? "" + selectID = values.joined(separator: ",") + case .file(let fileName): + fieldType = .file + value = fileName ?? "" + selectID = "" + case .select(let selectedValue, let values): + fieldType = .select + value = selectedValue ?? "" + selectID = values.joined(separator: ",") + } + + return Field(fieldID: field.fieldId, fieldType: fieldType, value: value, selectID: selectID) + } + return Object(groupId: group.groupId, fields: fields) } + return Content(sectionID: section.sectionId, objects: objects) + } + } + + return InputAuthenticationRequestDTO(contents: contents) + } - let object = Object( - fieldID: field.fieldId, - sectionType: sectionType, - value: value, - selectID: selectID - ) + func updateTextField(area: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, text: String) { + model?.updateTextField(area: area, sectionIndex: sectionIndex, groupIndex: groupIndex, fieldIndex: fieldIndex, text: text) + } - objects.append(object) - } + func updateNumberField(area: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, number: Int) { + model?.updateNumberField(area: area, sectionIndex: sectionIndex, groupIndex: groupIndex, fieldIndex: fieldIndex, number: number) + } - let content = Content(sectionID: section.sectionId, objects: objects) - contents.append(content) - } - } + func updateBoolField(area: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, select: String) { + model?.updateBoolField(area: area, sectionIndex: sectionIndex, groupIndex: groupIndex, fieldIndex: fieldIndex, select: select) + } + + func updateFileField(area: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, file: String) { + model?.updateFileField(area: area, sectionIndex: sectionIndex, groupIndex: groupIndex, fieldIndex: fieldIndex, file: file) + } - return InputAuthenticationRequestDTO(contents: contents) + func updateSelectField(area: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, select: String) { + model?.updateSelectField(area: area, sectionIndex: sectionIndex, groupIndex: groupIndex, fieldIndex: fieldIndex, select: select) } } @@ -120,15 +145,26 @@ extension SectionEntity { GSMAuthenticationFormUIModel.Area.Section.init( title: title, sectionId: sectionId, - currentFieldCount: maxCount, + maxCount: maxCount, + currentFieldCount: 0, + groups: groups.map { $0.toModel() } + ) + } +} + +extension GroupEntity { + func toModel() -> GSMAuthenticationFormUIModel.Area.Section.Group { + GSMAuthenticationFormUIModel.Area.Section.Group.init( + groupId: groupId, + maxScore: maxScore, fields: fields.map { $0.toModel() } ) } } extension FieldEntity { - func toModel() -> GSMAuthenticationFormUIModel.Area.Section.Field { - GSMAuthenticationFormUIModel.Area.Section.Field.init( + func toModel() -> GSMAuthenticationFormUIModel.Area.Section.Group.Field { + return GSMAuthenticationFormUIModel.Area.Section.Group.Field.init( fieldId: fieldId, type: transformType(type: type), scoreDescription: scoreDescription, @@ -136,13 +172,15 @@ extension FieldEntity { ) } - func transformType(type: FieldType) -> GSMAuthenticationFormUIModel.Area.Section.Field.FieldType { + func transformType(type: FieldType) -> GSMAuthenticationFormUIModel.Area.Section.Group.Field.FieldType { + let value: [String] = values?.map { $0.value } ?? [] + switch type { - case .text: .text(value: nil) - case .number: .number(value: nil) - case .boolean: .boolean(isSelcted: nil) - case .file: .file(fileName: nil) - case .select: .select(selectedValue: nil, values: []) + case .text: return .text(value: nil) + case .number: return .number(value: nil) + case .boolean: return .boolean(selectedValue: nil, values: value) + case .file: return .file(fileName: nil) + case .select: return .select(selectedValue: nil, values: value) } } } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift index b7b99ee4..c808ab31 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift @@ -3,4 +3,50 @@ import Foundation protocol GSMAuthenticationFormIntentProtocol { func onAppear() func saveButtonDidTap(state: any GSMAuthenticationFormStateProtocol) + func appendField( + area: Int, + sectionIndex: Int, + groupIndex: Int, + fields: [GSMAuthenticationFormUIModel.Area.Section.Group.Field] + ) + func deleteField( + area: Int, + sectionIndex: Int, + groupIndex: Int + ) + func updateTextField( + area: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + text: String + ) + func updateNumberField( + area: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + number: Int + ) + func updateBoolField( + area: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + select: String + ) + func updateFileField( + area: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + file: String + ) + func updateSelectField( + area: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + select: String + ) } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormFieldModel.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormFieldModel.swift index 1763d645..3725e310 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormFieldModel.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormFieldModel.swift @@ -1,10 +1,10 @@ struct GSMAuthenticationFormFieldModel { let sectionId: String - let sectionType: GSMAuthenticationFormUIModel.Area.Section.Field.FieldType + let sectionType: GSMAuthenticationFormUIModel.Area.Section.Group.Field.FieldType let objects: [Object] struct Object { let selectId: String - let value: GSMAuthenticationFormUIModel.Area.Section.Field.FieldType + let value: GSMAuthenticationFormUIModel.Area.Section.Group.Field.FieldType } } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift index 61482c38..d7d8c7aa 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift @@ -1,4 +1,5 @@ import Foundation +import AuthenticationDomainInterface import FoundationUtil final class GSMAuthenticationFormModel: ObservableObject, GSMAuthenticationFormStateProtocol { @@ -14,4 +15,46 @@ extension GSMAuthenticationFormModel: GSMAuthenticationFormActionProtocol { func updateFieldContents(fields: [GSMAuthenticationFormFieldModel]) { self.fieldContents = fields } + + func appendField( + area: Int, + sectionIndex: Int, + groupIndex: Int, + fields: [GSMAuthenticationFormUIModel.Area.Section.Group.Field] + ) { + uiModel.areas[area].sections[sectionIndex].groups[groupIndex].fields.append( + .init( + fieldId: fields.first?.fieldId ?? "", + type: fields.first?.type ?? .text(value: ""), + scoreDescription: fields.first?.scoreDescription, + placeholder: fields.first?.placeholder + ) + ) + } + + func updateTextField(area: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, text: String) { + uiModel.areas[area].sections[sectionIndex].groups[groupIndex].fields[fieldIndex].type = .text(value: text) + } + + func updateNumberField(area: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, number: Int) { + uiModel.areas[area].sections[sectionIndex].groups[groupIndex].fields[fieldIndex].type = .number(value: number) + } + + func updateBoolField(area: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, select: String) { + uiModel.areas[area].sections[sectionIndex].groups[groupIndex].fields[fieldIndex].type = + .boolean(selectedValue: select, values: uiModel.areas[area].sections[sectionIndex].groups[groupIndex].fields[fieldIndex].findFieldTypeValue()) + } + + func updateFileField(area: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, file: String) { + uiModel.areas[area].sections[sectionIndex].groups[groupIndex].fields[fieldIndex].type = .file(fileName: file) + } + + func updateSelectField(area: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, select: String) { + uiModel.areas[area].sections[sectionIndex].groups[groupIndex].fields[fieldIndex].type = + .select(selectedValue: select, values: uiModel.areas[area].sections[sectionIndex].groups[groupIndex].fields[fieldIndex].findFieldTypeValue()) + } + + func deleteField(area: Int, sectionIndex: Int, groupIndex: Int) { + uiModel.areas[area].sections[sectionIndex].groups[groupIndex].fields.remove(at: uiModel.areas[area].sections[sectionIndex].groups[groupIndex].fields.endIndex - 1) + } } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift index 90b6ca9b..8eae7a04 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift @@ -1,9 +1,57 @@ +import Foundation protocol GSMAuthenticationFormStateProtocol { var uiModel: GSMAuthenticationFormUIModel { get } - var fieldContents: [GSMAuthenticationFormFieldModel] { get } } protocol GSMAuthenticationFormActionProtocol: AnyObject { func updateGSMAuthenticationFormUIModel(uiModel: GSMAuthenticationFormUIModel) - func updateFieldContents(fields: [GSMAuthenticationFormFieldModel]) + + func appendField( + area: Int, + sectionIndex: Int, + groupIndex: Int, + fields: [GSMAuthenticationFormUIModel.Area.Section.Group.Field] + ) + + func updateTextField( + area: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + text: String + ) + func updateNumberField( + area: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + number: Int + ) + func updateBoolField( + area: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + select: String + ) + func updateFileField( + area: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + file: String + ) + func updateSelectField( + area: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + select: String + ) + + func deleteField( + area: Int, + sectionIndex: Int, + groupIndex: Int + ) } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift index cb23063b..8b5e295b 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift @@ -2,31 +2,49 @@ import Foundation // swiftlint: disable nesting struct GSMAuthenticationFormUIModel { - let areas: [Area] - let files: [File] + var areas: [Area] + var files: [File] struct Area { - let title: String - let sections: [Section] + var title: String + var sections: [Section] struct Section { - let title: String - let sectionId: String - let currentFieldCount: Double - let fields: [Field] - - struct Field { - let fieldId: String - let type: FieldType - let scoreDescription: String? - let placeholder: String? - - enum FieldType { - case text(value: String?) - case number(value: Int?) - case boolean(isSelcted: Bool?) - case file(fileName: String?) - case select(selectedValue: String?, values: [String]) + var title: String + var sectionId: String + var maxCount: Int + var currentFieldCount: Int + var groups: [Group] + + struct Group { + var groupId: String + var maxScore: Double + var fields: [Field] + + struct Field { + var fieldId: String + var type: FieldType + var scoreDescription: String? + var placeholder: String? + + enum FieldType { + case text(value: String?) + case number(value: Int?) + case boolean(selectedValue: String?, values: [String]) + case file(fileName: String?) + case select(selectedValue: String?, values: [String]) + } + + func findFieldTypeValue() -> [String] { + switch type { + case let .boolean(_, values): + return values + case let .select(_, values): + return values + default: + return [] + } + } } } } @@ -37,4 +55,5 @@ struct GSMAuthenticationFormUIModel { let url: String } } + // swiftlint: enable nesting diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift index 4ef71d7d..be78f4db 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift @@ -1,17 +1,19 @@ import DesignSystem import Foundation import SwiftUI +import BaseFeature +import ViewUtil enum FieldChanges { case text(String) case number(Int) - case boolean(Bool) - case file(URL) + case boolean(String) + case file(String) case select(String) } enum FieldInteraction { - case fieldChanges(key: String, fieldChanges: FieldChanges) + case fieldChanges(area: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, fieldChanges: FieldChanges) case fieldAdd(area: Int, section: Int, field: Int) } @@ -22,12 +24,16 @@ struct GSMAuthenticationFormBuilderView: View { private let onFieldInteraction: (FieldInteraction) -> Void private typealias Area = GSMAuthenticationFormUIModel.Area private typealias Section = Area.Section - private typealias Field = Section.Field + private typealias Group = Section.Group + private typealias Field = Group.Field + let intent: GSMAuthenticationFormIntentProtocol init( + intent: GSMAuthenticationFormIntentProtocol, uiModel: GSMAuthenticationFormUIModel, onFieldInteraction: @escaping (FieldInteraction) -> Void ) { + self.intent = intent self.uiModel = uiModel self.onFieldInteraction = onFieldInteraction } @@ -43,12 +49,12 @@ struct GSMAuthenticationFormBuilderView: View { @ViewBuilder private func areaList(areas: [Area]) -> some View { LazyVStack(spacing: 16) { - ForEach(uiModel.areas, id: \.title) { area in + ForEach(uiModel.areas.indices, id: \.self) { index in SMSSeparator() .padding(.bottom, 4) HStack(spacing: 16) { - SMSText(area.title, font: .title1) + SMSText(areas[index].title, font: .title1) Spacer() @@ -63,7 +69,7 @@ struct GSMAuthenticationFormBuilderView: View { } .padding(.horizontal, 20) - sectionList(sections: area.sections) + sectionList(areaIndex: index, sections: areas[index].sections) .padding(.horizontal, 20) } } @@ -71,73 +77,145 @@ struct GSMAuthenticationFormBuilderView: View { } @ViewBuilder - private func sectionList(sections: [Section]) -> some View { + private func sectionList(areaIndex: Int, sections: [Section]) -> some View { LazyVStack(spacing: 0) { - ForEach(sections, id: \.sectionId) { section in + ForEach(sections.indices, id: \.self) { index in VStack { - fieldList(key: section.sectionId, fields: section.fields) + groupList(areaIndex: areaIndex, sectionIndex: index, groups: sections[index].groups, maxCount: sections[index].maxCount) + } + .titleWrapper(sections[index].title) + .frame(maxWidth: .infinity) + } + .padding(.vertical, 20) + } + } - if section.currentFieldCount != 1 { - HStack { + @ViewBuilder + private func groupList(areaIndex: Int, sectionIndex: Int, groups: [Group], maxCount: Int) -> some View { + LazyVStack(spacing: 0) { + ForEach(groups.indices, id: \.self) { index in + VStack { + fieldList( + areaIndex: areaIndex, + sectionIndex: sectionIndex, + groupIndex: index, + fields: groups[index].fields + ) + + HStack { + ConditionView(maxCount > groups[index].fields.count) { SMSChip("추가") { + intent.appendField( + area: areaIndex, + sectionIndex: sectionIndex, + groupIndex: index, + fields: groups[index].fields + ) } + } - Spacer() + Spacer() - SMSIcon(.trash) + ConditionView(groups[index].fields.count > 1 && maxCount > 1) { + Button { + intent.deleteField( + area: areaIndex, + sectionIndex: sectionIndex, + groupIndex: index + ) + } label: { + SMSIcon(.trash) + } } } } - .titleWrapper(section.title) - .frame(maxWidth: .infinity) } - .padding(.vertical, 20) } } @ViewBuilder - private func fieldList(key: String, fields: [Field]) -> some View { - LazyVStack(spacing: 16) { - ForEach(fields, id: \.fieldId) { field in - fieldView(key: field.fieldId, field: field) - .titleWrapper( - field.scoreDescription ?? "", - position: .bottom(.leading), - font: .caption1, - color: .neutral(.n30) - ) + private func fieldList(areaIndex: Int, sectionIndex: Int, groupIndex: Int, fields: [Field]) -> some View { + LazyVStack(spacing: 0) { + ForEach(fields.indices, id: \.self) { index in + fieldView( + areaIndex: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: index, + field: fields[index] + ) + .titleWrapper( + fields[index].scoreDescription ?? "", + position: .bottom(.leading), + font: .caption1, + color: .neutral(.n30) + ) } } } @ViewBuilder - private func fieldView(key: String, field: Field) -> some View { + private func fieldView(areaIndex: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, field: Field) -> some View { switch field.type { - case let .text(value): - textTypeFieldView(key: key, placeholder: field.placeholder, text: value) + case let .text(value): + textTypeFieldView( + areaIndex: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + placeholder: field.placeholder, + text: value + ) - case let .number(value): - numberTypeFieldView(key: key, placeholder: field.placeholder, number: value) + case let .number(value): + numberTypeFieldView( + areaIndex: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + placeholder: field.placeholder, + number: value + ) - case let .boolean(isSelected): - booleanTypeFieldView(key: key, isSelected: isSelected) + case let .boolean(selectedValue, values): + booleanTypeFieldView( + areaIndex: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + selectedValue: selectedValue, + values: values + ) - case let .file(fileName): - fileTypeFieldView(key: key, placeholder: field.placeholder, fileName: fileName) + case let .file(fileName): + fileTypeFieldView( + areaIndex: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + placeholder: field.placeholder, + fileName: fileName + ) - case let .select(selectedValue, values): - selectTypeFieldView( - key: key, - placeholder: field.placeholder, - selectedValue: selectedValue, - values: values + case let .select(selectedValue, values): + selectTypeFieldView( + areaIndex: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + placeholder: field.placeholder, + selectedValue: selectedValue, + values: values ) } } @ViewBuilder private func textTypeFieldView( - key: String, + areaIndex: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, placeholder: String?, text: String? ) -> some View { @@ -146,7 +224,15 @@ struct GSMAuthenticationFormBuilderView: View { text: Binding( get: { text ?? "" }, set: { newValue in - onFieldInteraction(.fieldChanges(key: key, fieldChanges: .text(newValue))) + onFieldInteraction( + .fieldChanges( + area: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + fieldChanges: .text(newValue) + ) + ) } ) ) @@ -154,7 +240,10 @@ struct GSMAuthenticationFormBuilderView: View { @ViewBuilder private func numberTypeFieldView( - key: String, + areaIndex: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, placeholder: String?, number: Int? ) -> some View { @@ -170,7 +259,15 @@ struct GSMAuthenticationFormBuilderView: View { }, set: { newValue in guard let numberValue = Int(newValue) else { return } - onFieldInteraction(.fieldChanges(key: key, fieldChanges: .number(numberValue))) + onFieldInteraction( + .fieldChanges( + area: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + fieldChanges: .number(numberValue) + ) + ) } ) ) @@ -178,19 +275,39 @@ struct GSMAuthenticationFormBuilderView: View { @ViewBuilder private func booleanTypeFieldView( - key: String, - isSelected: Bool? + areaIndex: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + selectedValue: String?, + values: [String] ) -> some View { SMSSegmentedControl( - options: ["True", "False"], - selectedOption: (isSelected ?? false) ? "True" : "False" + options: values, + selectedOption: selectedValue ) { option in switch option { - case "True": - onFieldInteraction(.fieldChanges(key: key, fieldChanges: .boolean(true))) + case values[0]: + onFieldInteraction( + .fieldChanges( + area: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + fieldChanges: .boolean(values[0]) + ) + ) - case "False": - onFieldInteraction(.fieldChanges(key: key, fieldChanges: .boolean(false))) + case values[1]: + onFieldInteraction( + .fieldChanges( + area: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + fieldChanges: .boolean(values[1]) + ) + ) default: return @@ -200,7 +317,10 @@ struct GSMAuthenticationFormBuilderView: View { @ViewBuilder private func fileTypeFieldView( - key: String, + areaIndex: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, placeholder: String?, fileName: String? ) -> some View { @@ -210,7 +330,15 @@ struct GSMAuthenticationFormBuilderView: View { ) { result in switch result { case let .success(url): - onFieldInteraction(.fieldChanges(key: key, fieldChanges: .file(url))) + onFieldInteraction( + .fieldChanges( + area: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + fieldChanges: .file(url.absoluteString) + ) + ) default: return @@ -220,7 +348,10 @@ struct GSMAuthenticationFormBuilderView: View { @ViewBuilder private func selectTypeFieldView( - key: String, + areaIndex: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, placeholder: String?, selectedValue: String?, values: [String] @@ -230,7 +361,8 @@ struct GSMAuthenticationFormBuilderView: View { text: Binding( get: { selectedValue ?? "" }, set: { _ in } - ) + ), + isOnClear: false ) .disabled(true) .overlay(alignment: .trailing) { @@ -243,7 +375,15 @@ struct GSMAuthenticationFormBuilderView: View { optionPickerPresenter.presentOptionPicker( options: values, onOptionSelect: { selectedOption in - onFieldInteraction(.fieldChanges(key: key, fieldChanges: .select(selectedOption))) + onFieldInteraction( + .fieldChanges( + area: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + fieldChanges: .select(selectedOption) + ) + ) } ) } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift index 7f8317cc..d6a23c58 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift @@ -2,13 +2,18 @@ import BaseFeature import SwiftUI import DesignSystem import ViewUtil +import Combine struct GSMAuthenticationFormView: View { @Environment(\.dismiss) var dismiss @Environment(\.safeAreaInsets) var safeAreaInsets + @Environment(\.optionPickerPresenter) var optionPickerPresenter @StateObject var container: MVIContainer + @State private var cancellables = Set() var intent: any GSMAuthenticationFormIntentProtocol { container.intent } var state: any GSMAuthenticationFormStateProtocol { container.model } + @State private var isPresented: Bool = false + @State private var selectValues: [String] = [] init( container: MVIContainer @@ -18,7 +23,24 @@ struct GSMAuthenticationFormView: View { var body: some View { ZStack { - GSMAuthenticationFormBuilderView(uiModel: state.uiModel) { _ in + GSMAuthenticationFormBuilderView(intent: intent, uiModel: state.uiModel) { field in + switch field { + case let .fieldChanges(area, section, group, field, fieldChanges): + switch fieldChanges { + case let .text(text): + intent.updateTextField(area: area, sectionIndex: section, groupIndex: group, fieldIndex: field, text: text) + case let .number(number): + intent.updateNumberField(area: area, sectionIndex: section, groupIndex: group, fieldIndex: field, number: number) + case let .boolean(select): + intent.updateBoolField(area: area, sectionIndex: section, groupIndex: group, fieldIndex: field, select: select) + case let .file(file): + intent.updateFileField(area: area, sectionIndex: section, groupIndex: group, fieldIndex: field, file: file) + case let .select(select): + intent.updateSelectField(area: area, sectionIndex: section, groupIndex: group, fieldIndex: field, select: select) + } + case let .fieldAdd(area, section, field): + break + } } .padding(.bottom, safeAreaInsets.bottom + 16) .onAppear { @@ -35,7 +57,55 @@ struct GSMAuthenticationFormView: View { .smsBackButton( dismiss: dismiss ) + .smsBottomSheet( + isShowing: Binding( + get: { isPresented }, + set: { _ in } + ) + ) { + valueListView() + } .navigationBarTitleDisplayMode(.inline) + .onAppear { + subscribeToIsPresentedPublisher() + } + } + } + + func subscribeToIsPresentedPublisher() { + optionPickerPresenter.isPresentedPublisher + .receive(on: DispatchQueue.main) + .sink { newValue in + isPresented = newValue + } + .store(in: &cancellables) + + optionPickerPresenter.optionsPublisher + .receive(on: DispatchQueue.main) + .sink { newValue in + selectValues = newValue + } + .store(in: &cancellables) + } + + @ViewBuilder + func valueListView() -> some View { + ScrollView { + LazyVStack(spacing: 8) { + ForEach(selectValues, id: \.self) { value in + HStack { + Text(value) + .smsFont(.body1, color: .neutral(.n50)) + + Spacer() + } + .padding(.vertical, 13.5) + .padding(.horizontal, 20) + .buttonWrapper { + optionPickerPresenter.sendSelectedOption(option: value) + } + } + } } } } diff --git a/Projects/Feature/MainFeature/Sources/Intent/MainIntent.swift b/Projects/Feature/MainFeature/Sources/Intent/MainIntent.swift index 7a708753..08299707 100644 --- a/Projects/Feature/MainFeature/Sources/Intent/MainIntent.swift +++ b/Projects/Feature/MainFeature/Sources/Intent/MainIntent.swift @@ -94,6 +94,22 @@ final class MainIntent: MainIntentProtocol { func exitIsDismissed() { model?.updateIsPresentedExitDialog(isPresented: false) } + + func myInfoBottomSheetIsRequired() { + model?.updateIsPresentedMyInfoBottomSheet(isPresented: true) + } + + func myInfoBottomSheetIsDismissed() { + model?.updateIsPresentedMyInfoBottomSheet(isPresented: false) + } + + func authentificationPageIsRequired() { + model?.updateIsPresentedAuthentification(isPresented: true) + } + + func authentificationPageIsDismissed() { + model?.updateIsPresentedAuthentification(isPresented: false) + } } extension MainIntent: FilterDelegate { diff --git a/Projects/Feature/MainFeature/Sources/Intent/MainIntentProtocol.swift b/Projects/Feature/MainFeature/Sources/Intent/MainIntentProtocol.swift index 29011df7..2b3e1f0d 100644 --- a/Projects/Feature/MainFeature/Sources/Intent/MainIntentProtocol.swift +++ b/Projects/Feature/MainFeature/Sources/Intent/MainIntentProtocol.swift @@ -15,4 +15,8 @@ protocol MainIntentProtocol: FilterDelegate, MyPageDelegate, GSMAuthenticationDe func studentDetailDismissed() func exitIsRequired() func exitIsDismissed() + func myInfoBottomSheetIsRequired() + func myInfoBottomSheetIsDismissed() + func authentificationPageIsRequired() + func authentificationPageIsDismissed() } diff --git a/Projects/Feature/MainFeature/Sources/Model/MainModel.swift b/Projects/Feature/MainFeature/Sources/Model/MainModel.swift index e1e83d0d..68983053 100644 --- a/Projects/Feature/MainFeature/Sources/Model/MainModel.swift +++ b/Projects/Feature/MainFeature/Sources/Model/MainModel.swift @@ -27,6 +27,8 @@ final class MainModel: ObservableObject, MainStateProtocol { @Published var _content: [SingleStudentEntity] = [] @Published var isPresentedFilterPage: Bool = false @Published var isPresentedMyPage: Bool = false + @Published var isPresentedMyInfoBottomSheet: Bool = false + @Published var isPresentedAuthentification: Bool = false @Published var isPresntedExit: Bool = false @Published var selectedUserID: String? @Published var currentUserRole: UserRoleType = .guest @@ -59,6 +61,14 @@ extension MainModel: MainActionProtocol { self.isPresntedExit = isPresented } + func updateIsPresentedMyInfoBottomSheet(isPresented: Bool) { + self.isPresentedMyInfoBottomSheet = isPresented + } + + func updateIsPresentedAuthentification(isPresented: Bool) { + self.isPresentedAuthentification = isPresented + } + func appendContent(content: [SingleStudentEntity]) { self.content.append(contentsOf: content) } diff --git a/Projects/Feature/MainFeature/Sources/Model/MainModelProtocol.swift b/Projects/Feature/MainFeature/Sources/Model/MainModelProtocol.swift index a1c57ced..3e2b9833 100644 --- a/Projects/Feature/MainFeature/Sources/Model/MainModelProtocol.swift +++ b/Projects/Feature/MainFeature/Sources/Model/MainModelProtocol.swift @@ -9,6 +9,8 @@ protocol MainStateProtocol { var isPresentedFilterPage: Bool { get } var isPresentedMyPage: Bool { get } var isPresntedExit: Bool { get } + var isPresentedAuthentification: Bool { get } + var isPresentedMyInfoBottomSheet: Bool { get } var content: [SingleStudentEntity] { get } var selectedUserID: String? { get } var currentUserRole: UserRoleType { get } @@ -22,6 +24,8 @@ protocol MainActionProtocol: AnyObject { func updateIsPresentedFilterPage(isPresented: Bool) func updateIsPresentedMypage(isPresented: Bool) func updateIsPresentedExitDialog(isPresented: Bool) + func updateIsPresentedMyInfoBottomSheet(isPresented: Bool) + func updateIsPresentedAuthentification(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 ac2c2f47..2e9e1e20 100644 --- a/Projects/Feature/MainFeature/Sources/Scene/MainView.swift +++ b/Projects/Feature/MainFeature/Sources/Scene/MainView.swift @@ -120,12 +120,19 @@ struct MainView: View { ) ) .navigate( - to: gsmAuthenticatoinBuildable.makeView().eraseToAnyView(), + to: myPageBuildable.makeView(delegate: intent).eraseToAnyView(), when: Binding( get: { state.isPresentedMyPage }, set: { _ in intent.myPageDismissed() } ) ) + .navigate( + to: gsmAuthenticatoinBuildable.makeView().eraseToAnyView(), + when: Binding( + get: { state.isPresentedAuthentification }, + set: { _ in intent.authentificationPageIsDismissed() } + ) + ) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { @@ -138,7 +145,7 @@ struct MainView: View { SMSIcon(.profile, width: 32, height: 32) .clipShape(Circle()) .onTapGesture { - state.currentUserRole == .student ? intent.myPageIsRequired() : intent.exitIsRequired() + state.currentUserRole == .student ? intent.myInfoBottomSheetIsRequired() : intent.exitIsRequired() } } } @@ -164,6 +171,45 @@ struct MainView: View { } ] ) + .smsBottomSheet( + isShowing: Binding( + get: { state.isPresentedMyInfoBottomSheet }, + set: { _ in intent.myInfoBottomSheetIsDismissed() } + ) + ) { + VStack(alignment: .leading, spacing: 32) { + Button { + intent.myInfoBottomSheetIsDismissed() + intent.myPageIsRequired() + } label: { + HStack(spacing: 12) { + SMSIcon(.person) + + SMSText("마이페이지", font: .title2) + .foregroundStyle(Color.sms(.neutral(.n50))) + + Spacer() + } + } + + Button { + intent.myInfoBottomSheetIsDismissed() + intent.authentificationPageIsRequired() + } label: { + HStack(spacing: 12) { + SMSIcon(.briefcases) + + SMSText("인증제", font: .title2) + .foregroundStyle(Color.sms(.neutral(.n50))) + + Spacer() + } + } + } + .padding(.top, 12) + .padding(.horizontal, 20) + } + .animation(.default, value: state.isPresentedMyInfoBottomSheet) } .navigationViewStyle(.stack) } From ced56eca2cde15522bc7748fb776e4aa61e97dea Mon Sep 17 00:00:00 2001 From: baegteun Date: Sat, 6 Jul 2024 20:17:06 +0900 Subject: [PATCH 03/13] :recycle: :: [#342] group add --- .../Sources/Application/NeedleGenerated.swift | 56 ++++----- .../FetchAuthenticationFormUseCaseSpy.swift | 82 +++++++++++++ .../InputAuthenticationUseCaseSpy.swift | 13 +++ .../Testing/Testing.swift | 1 - .../Demo/Sources/AppDelegate.swift | 21 +++- .../Project.swift | 6 +- .../Intent/GSMAuthenticationFormIntent.swift | 108 ++++++++++-------- .../GSMAuthenticationFormIntentProtocol.swift | 3 +- .../Model/GSMAuthenticationFormModel.swift | 38 ++++-- .../GSMAuthenticationFormModelProtocol.swift | 7 +- .../Model/GSMAuthenticationFormUIModel.swift | 10 ++ .../GSMAuthenticationFormBuilderView.swift | 22 ++-- .../Scene/GSMAuthenticationFormView.swift | 6 +- 13 files changed, 266 insertions(+), 107 deletions(-) create mode 100644 Projects/Domain/AuthenticationDomain/Testing/FetchAuthenticationFormUseCaseSpy.swift create mode 100644 Projects/Domain/AuthenticationDomain/Testing/InputAuthenticationUseCaseSpy.swift delete mode 100644 Projects/Domain/AuthenticationDomain/Testing/Testing.swift diff --git a/Projects/App/Sources/Application/NeedleGenerated.swift b/Projects/App/Sources/Application/NeedleGenerated.swift index f29b6191..2716594e 100644 --- a/Projects/App/Sources/Application/NeedleGenerated.swift +++ b/Projects/App/Sources/Application/NeedleGenerated.swift @@ -471,33 +471,33 @@ extension JwtStoreComponent: Registration { extension AppComponent: Registration { public func registerItems() { - localTable["rootComponent-RootComponent"] = { self.rootComponent as Any } - localTable["signinBuildable-any SigninBuildable"] = { self.signinBuildable as Any } - localTable["inputInformationBuildable-any InputInformationBuildable"] = { self.inputInformationBuildable as Any } - localTable["inputProfileInfoBuildable-any InputProfileInfoBuildable"] = { self.inputProfileInfoBuildable as Any } - localTable["inputSchoolLifeInfoBuildable-any InputSchoolLifeInfoBuildable"] = { self.inputSchoolLifeInfoBuildable as Any } - localTable["inputWorkInfoBuildable-any InputWorkInfoBuildable"] = { self.inputWorkInfoBuildable as Any } - localTable["inputMilitaryInfoBuildable-any InputMilitaryInfoBuildable"] = { self.inputMilitaryInfoBuildable as Any } - localTable["inputCertificateInfoBuildable-any InputCertificateInfoBuildable"] = { self.inputCertificateInfoBuildable as Any } - localTable["inputLanguageInfoBuildable-any InputLanguageInfoBuildable"] = { self.inputLanguageInfoBuildable as Any } - localTable["inputPrizeInfoBuildable-any InputPrizeInfoBuildable"] = { self.inputPrizeInfoBuildable as Any } - localTable["inputProjectInfoBuildable-any InputProjectInfoBuildable"] = { self.inputProjectInfoBuildable as Any } - localTable["mainBuildable-any MainBuildable"] = { self.mainBuildable as Any } - localTable["myPageBuildable-any MyPageBuildable"] = { self.myPageBuildable as Any } - localTable["techStackAppendBuildable-any TechStackAppendBuildable"] = { self.techStackAppendBuildable as Any } - localTable["studentDetailBuildable-any StudentDetailBuildable"] = { self.studentDetailBuildable as Any } - localTable["filterBuildable-any FilterBuildable"] = { self.filterBuildable as Any } - localTable["splashBuildable-any SplashBuildable"] = { self.splashBuildable as Any } - localTable["gsmAuthenticationBuildable-any GSMAuthenticationBuildable"] = { self.gsmAuthenticationBuildable as Any } - localTable["authDomainBuildable-any AuthDomainBuildable"] = { self.authDomainBuildable as Any } - localTable["studentDomainBuildable-any StudentDomainBuildable"] = { self.studentDomainBuildable as Any } - localTable["majorDomainBuildable-any MajorDomainBuildable"] = { self.majorDomainBuildable as Any } - localTable["fileDomainBuildable-any FileDomainBuildable"] = { self.fileDomainBuildable as Any } - localTable["userDomainBuildable-any UserDomainBuildable"] = { self.userDomainBuildable as Any } - localTable["techStackDomainBuildable-any TechStackDomainBuildable"] = { self.techStackDomainBuildable as Any } - localTable["jwtStoreBuildable-any JwtStoreBuildable"] = { self.jwtStoreBuildable as Any } - localTable["keychainBuildable-any KeychainBuildable"] = { self.keychainBuildable as Any } - localTable["authenticationDomainBuildable-any AuthenticationDomainBuildable"] = { self.authenticationDomainBuildable as Any } + localTable["rootComponent-RootComponent"] = { [unowned self] in self.rootComponent as Any } + localTable["signinBuildable-any SigninBuildable"] = { [unowned self] in self.signinBuildable as Any } + localTable["inputInformationBuildable-any InputInformationBuildable"] = { [unowned self] in self.inputInformationBuildable as Any } + localTable["inputProfileInfoBuildable-any InputProfileInfoBuildable"] = { [unowned self] in self.inputProfileInfoBuildable as Any } + localTable["inputSchoolLifeInfoBuildable-any InputSchoolLifeInfoBuildable"] = { [unowned self] in self.inputSchoolLifeInfoBuildable as Any } + localTable["inputWorkInfoBuildable-any InputWorkInfoBuildable"] = { [unowned self] in self.inputWorkInfoBuildable as Any } + localTable["inputMilitaryInfoBuildable-any InputMilitaryInfoBuildable"] = { [unowned self] in self.inputMilitaryInfoBuildable as Any } + localTable["inputCertificateInfoBuildable-any InputCertificateInfoBuildable"] = { [unowned self] in self.inputCertificateInfoBuildable as Any } + localTable["inputLanguageInfoBuildable-any InputLanguageInfoBuildable"] = { [unowned self] in self.inputLanguageInfoBuildable as Any } + localTable["inputPrizeInfoBuildable-any InputPrizeInfoBuildable"] = { [unowned self] in self.inputPrizeInfoBuildable as Any } + localTable["inputProjectInfoBuildable-any InputProjectInfoBuildable"] = { [unowned self] in self.inputProjectInfoBuildable as Any } + localTable["mainBuildable-any MainBuildable"] = { [unowned self] in self.mainBuildable as Any } + localTable["myPageBuildable-any MyPageBuildable"] = { [unowned self] in self.myPageBuildable as Any } + localTable["techStackAppendBuildable-any TechStackAppendBuildable"] = { [unowned self] in self.techStackAppendBuildable as Any } + localTable["studentDetailBuildable-any StudentDetailBuildable"] = { [unowned self] in self.studentDetailBuildable as Any } + localTable["filterBuildable-any FilterBuildable"] = { [unowned self] in self.filterBuildable as Any } + localTable["splashBuildable-any SplashBuildable"] = { [unowned self] in self.splashBuildable as Any } + localTable["gsmAuthenticationBuildable-any GSMAuthenticationBuildable"] = { [unowned self] in self.gsmAuthenticationBuildable as Any } + localTable["authDomainBuildable-any AuthDomainBuildable"] = { [unowned self] in self.authDomainBuildable as Any } + localTable["studentDomainBuildable-any StudentDomainBuildable"] = { [unowned self] in self.studentDomainBuildable as Any } + localTable["majorDomainBuildable-any MajorDomainBuildable"] = { [unowned self] in self.majorDomainBuildable as Any } + localTable["fileDomainBuildable-any FileDomainBuildable"] = { [unowned self] in self.fileDomainBuildable as Any } + localTable["userDomainBuildable-any UserDomainBuildable"] = { [unowned self] in self.userDomainBuildable as Any } + localTable["techStackDomainBuildable-any TechStackDomainBuildable"] = { [unowned self] in self.techStackDomainBuildable as Any } + localTable["jwtStoreBuildable-any JwtStoreBuildable"] = { [unowned self] in self.jwtStoreBuildable as Any } + localTable["keychainBuildable-any KeychainBuildable"] = { [unowned self] in self.keychainBuildable as Any } + localTable["authenticationDomainBuildable-any AuthenticationDomainBuildable"] = { [unowned self] in self.authenticationDomainBuildable as Any } } } extension KeychainComponent: Registration { @@ -665,7 +665,7 @@ private func registerProviderFactory(_ componentPath: String, _ factory: @escapi #if !NEEDLE_DYNAMIC -private func register1() { +@inline(never) private func register1() { registerProviderFactory("^->AppComponent->JwtStoreComponent", factoryb27d5aae1eb7e73575a6f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent", factoryEmptyDependencyProvider) registerProviderFactory("^->AppComponent->KeychainComponent", factoryEmptyDependencyProvider) diff --git a/Projects/Domain/AuthenticationDomain/Testing/FetchAuthenticationFormUseCaseSpy.swift b/Projects/Domain/AuthenticationDomain/Testing/FetchAuthenticationFormUseCaseSpy.swift new file mode 100644 index 00000000..e133ab7b --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Testing/FetchAuthenticationFormUseCaseSpy.swift @@ -0,0 +1,82 @@ +import AuthenticationDomainInterface + +public final class FetchAuthenticationFormUseCaseSpy: FetchAuthenticationFormUseCase { + public var callCount = 0 + public var handler: (() async throws -> AuthenticationFormEntity)? = { + return AuthenticationFormEntity( + files: [ + AuthenticationFormEntity.File(name: "File 1", url: "http://example.com/file1"), + AuthenticationFormEntity.File(name: "File 2", url: "http://example.com/file2") + ], + areas: [ + AuthenticationFormEntity.Area( + title: "Area 1", + sections: [ + SectionEntity( + sectionId: "1", + title: "Section 1", + maxCount: 5, + groups: [ + GroupEntity( + groupId: "group1", + maxScore: 100.0, + fields: [ + FieldEntity( + fieldId: "field1", + type: FieldType.text, + scoreDescription: "Description 1", + values: nil, + placeholder: "Enter Text" + ), + FieldEntity( + fieldId: "field2", + type: FieldType.number, + scoreDescription: "Description 2", + values: nil, + placeholder: "Enter Number" + ), + FieldEntity( + fieldId: "field3", + type: FieldType.select, + scoreDescription: "Description 3", + values: [ + ValueEntity(selectId: "f3s1", value: "Option1"), + ValueEntity(selectId: "f3s2", value: "Option2"), + ValueEntity(selectId: "f3s3", value: "Option3") + ], + placeholder: "Select Select" + ), + FieldEntity( + fieldId: "field4", + type: FieldType.boolean, + scoreDescription: "Description 3", + values: [ + ValueEntity(selectId: "f4s1", value: "Option1"), + ValueEntity(selectId: "f4s2", value: "Option2") + ], + placeholder: "Select boolean" + ), + FieldEntity( + fieldId: "field5", + type: FieldType.file, + scoreDescription: "Description 4", + values: nil, + placeholder: "Select file" + ), + ] + ) + ] + ) + ] + ) + ] + ) + } + + public init() {} + + public func execute() async throws -> AuthenticationFormEntity { + guard let handler else { fatalError() } + return try await handler() + } +} diff --git a/Projects/Domain/AuthenticationDomain/Testing/InputAuthenticationUseCaseSpy.swift b/Projects/Domain/AuthenticationDomain/Testing/InputAuthenticationUseCaseSpy.swift new file mode 100644 index 00000000..d73daae9 --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Testing/InputAuthenticationUseCaseSpy.swift @@ -0,0 +1,13 @@ +import AuthenticationDomainInterface + +public final class InputAuthenticationUseCaseSpy: InputAuthenticationUseCase { + public var callCount = 0 + public var handler: ((InputAuthenticationRequestDTO) async throws -> Void)? + + public init() {} + + public func execute(req: InputAuthenticationRequestDTO) async throws { + callCount += 1 + try await handler?(req) + } +} diff --git a/Projects/Domain/AuthenticationDomain/Testing/Testing.swift b/Projects/Domain/AuthenticationDomain/Testing/Testing.swift deleted file mode 100644 index b1853ce6..00000000 --- a/Projects/Domain/AuthenticationDomain/Testing/Testing.swift +++ /dev/null @@ -1 +0,0 @@ -// This is for Tuist diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift index 0012d34d..be551369 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift @@ -1,8 +1,11 @@ import SwiftUI @testable import GSMAuthenticationFormFeature +import AuthenticationDomainTesting @main struct GSMAuthenticationFormDemoApp: App { + let make: DemoMakable = DemoMakable() + var body: some Scene { WindowGroup { let uiModel = GSMAuthenticationFormUIModel( @@ -69,9 +72,21 @@ struct GSMAuthenticationFormDemoApp: App { ] ) -// GSMAuthenticationFormBuilderView(intent: , uiModel: uiModel) { _ in -// -// } + make.makeView(uiModel: uiModel) + } + } +} + +final class DemoMakable { + func makeView(uiModel: GSMAuthenticationFormUIModel) -> GSMAuthenticationFormBuilderView { + return GSMAuthenticationFormBuilderView( + intent: GSMAuthenticationFormIntent( + model: GSMAuthenticationFormModel(), + fetchAuthenticationFormUseCase: FetchAuthenticationFormUseCaseSpy(), + inputAuthenticationUseCase: InputAuthenticationUseCaseSpy() + ), + uiModel: uiModel + ) { _ in } } } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Project.swift b/Projects/Feature/GSMAuthenticationFormFeature/Project.swift index 63488bef..727e6169 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Project.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Project.swift @@ -7,6 +7,10 @@ let project = Project.makeModule( product: .staticLibrary, targets: [.interface, .unitTest, .demo], internalDependencies: [ - .Feature.BaseFeature + .Feature.BaseFeature, + .Domain.AuthenticationDomainInterface + ], + demoDependencies: [ + .Domain.AuthenticationDomainTesting ] ) diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift index 8970d54a..838a49ee 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift @@ -21,14 +21,12 @@ final class GSMAuthenticationFormIntent: GSMAuthenticationFormIntentProtocol { func appendField( area: Int, sectionIndex: Int, - groupIndex: Int, - fields: [GSMAuthenticationFormUIModel.Area.Section.Group.Field] + groupIndex: Int ) { model?.appendField( area: area, sectionIndex: sectionIndex, - groupIndex: groupIndex, - fields: fields + groupIndex: groupIndex ) } @@ -39,12 +37,13 @@ final class GSMAuthenticationFormIntent: GSMAuthenticationFormIntentProtocol { func onAppear() { Task { do { - let uiModel = try await fetchAuthenticationFormUseCase.execute() + let authenticationEntity = try await fetchAuthenticationFormUseCase.execute() + model?.updateAuthenticationEntity(authenticationEntity: authenticationEntity) model?.updateGSMAuthenticationFormUIModel( uiModel: .init( - areas: uiModel.areas.map { $0.toModel() }, - files: uiModel.files.map { $0.toModel() } + areas: authenticationEntity.areas.map { $0.toModel() }, + files: authenticationEntity.files.map { $0.toModel() } ) ) } @@ -54,54 +53,73 @@ final class GSMAuthenticationFormIntent: GSMAuthenticationFormIntentProtocol { func saveButtonDidTap(state: any GSMAuthenticationFormStateProtocol) { Task { do { + guard let authenticationEntity = state.authenticationEntity else { return } try await inputAuthenticationUseCase.execute( - req: convertToDTO(model: state.uiModel) + req: convertToDTO( + model: state.uiModel, + entity: authenticationEntity + ) ) } } } - func convertToDTO(model: GSMAuthenticationFormUIModel) -> InputAuthenticationRequestDTO { - let contents: [Content] = model.areas.flatMap { area in - area.sections.map { section in - let objects: [Object] = section.groups.map { group in - let fields: [Field] = group.fields.map { field in - let fieldType: FieldType - let value: String - let selectID: String - - switch field.type { - case .text(let textValue): - fieldType = .text - value = textValue ?? "" - selectID = "" - case .number(let numberValue): - fieldType = .number - value = numberValue.map { String($0) } ?? "" - selectID = "" - case .boolean(let selectedValue, let values): - fieldType = .boolean - value = selectedValue ?? "" - selectID = values.joined(separator: ",") - case .file(let fileName): - fieldType = .file - value = fileName ?? "" - selectID = "" - case .select(let selectedValue, let values): - fieldType = .select - value = selectedValue ?? "" - selectID = values.joined(separator: ",") - } - - return Field(fieldID: field.fieldId, fieldType: fieldType, value: value, selectID: selectID) + func convertToDTO( + model: GSMAuthenticationFormUIModel, + entity: AuthenticationFormEntity + ) -> InputAuthenticationRequestDTO { + let contents: [Content] = model.areas.enumerated().flatMap { areaIndex, area in + area.sections.enumerated().map { sectionIndex, section in + let objects: [Object] = section.groups.enumerated().map { objectIndex, group in + let fields: [Field] = group.fields.enumerated().map { fieldIndex, field in + let fieldType: FieldType + let value: String + let selectID: String + + switch field.type { + case .text(let textValue): + fieldType = .text + value = textValue ?? "" + selectID = "" + case .number(let numberValue): + fieldType = .number + value = numberValue.map { String($0) } ?? "" + selectID = "" + case .boolean(let selectedValue, let values): + fieldType = .boolean + value = selectedValue ?? "" + selectID = entity.areas[areaIndex] + .sections[sectionIndex] + .groups[objectIndex] + .fields + .first { $0.fieldId == field.fieldId }? + .values? + .first { $0.value == selectedValue }?.selectId ?? "" + case .file(let fileName): + fieldType = .file + value = fileName ?? "" + selectID = "" + case .select(let selectedValue, let values): + fieldType = .select + value = selectedValue ?? "" + selectID = entity.areas[areaIndex] + .sections[sectionIndex] + .groups[objectIndex] + .fields + .first { $0.fieldId == field.fieldId }? + .values? + .first { $0.value == selectedValue }?.selectId ?? "" } - return Object(groupId: group.groupId, fields: fields) + + return Field(fieldID: field.fieldId, fieldType: fieldType, value: value, selectID: selectID) } - return Content(sectionID: section.sectionId, objects: objects) + return Object(groupId: group.groupId, fields: fields) } + return Content(sectionID: section.sectionId, objects: objects) } - - return InputAuthenticationRequestDTO(contents: contents) + } + + return InputAuthenticationRequestDTO(contents: contents) } func updateTextField(area: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, text: String) { diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift index c808ab31..74cc2f03 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift @@ -6,8 +6,7 @@ protocol GSMAuthenticationFormIntentProtocol { func appendField( area: Int, sectionIndex: Int, - groupIndex: Int, - fields: [GSMAuthenticationFormUIModel.Area.Section.Group.Field] + groupIndex: Int ) func deleteField( area: Int, diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift index d7d8c7aa..4c8c57b2 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift @@ -5,6 +5,7 @@ import FoundationUtil final class GSMAuthenticationFormModel: ObservableObject, GSMAuthenticationFormStateProtocol { @Published var uiModel: GSMAuthenticationFormUIModel = .init(areas: [], files: []) @Published var fieldContents: [GSMAuthenticationFormFieldModel] = [] + var authenticationEntity: AuthenticationFormEntity? } extension GSMAuthenticationFormModel: GSMAuthenticationFormActionProtocol { @@ -12,6 +13,10 @@ extension GSMAuthenticationFormModel: GSMAuthenticationFormActionProtocol { self.uiModel = uiModel } + func updateAuthenticationEntity(authenticationEntity: AuthenticationFormEntity) { + self.authenticationEntity = authenticationEntity + } + func updateFieldContents(fields: [GSMAuthenticationFormFieldModel]) { self.fieldContents = fields } @@ -19,17 +24,25 @@ extension GSMAuthenticationFormModel: GSMAuthenticationFormActionProtocol { func appendField( area: Int, sectionIndex: Int, - groupIndex: Int, - fields: [GSMAuthenticationFormUIModel.Area.Section.Group.Field] + groupIndex: Int ) { - uiModel.areas[area].sections[sectionIndex].groups[groupIndex].fields.append( + let selectedGroup: GSMAuthenticationFormUIModel.Area.Section.Group = uiModel.areas[area].sections[sectionIndex].groups[groupIndex] + uiModel.areas[area].sections[sectionIndex].groups.insert( .init( - fieldId: fields.first?.fieldId ?? "", - type: fields.first?.type ?? .text(value: ""), - scoreDescription: fields.first?.scoreDescription, - placeholder: fields.first?.placeholder - ) + groupId: selectedGroup.groupId, + maxScore: selectedGroup.maxScore, + fields: selectedGroup.fields.map { + GSMAuthenticationFormUIModel.Area.Section.Group.Field.init( + fieldId: $0.fieldId, + type: $0.type.emptyValue, + scoreDescription: $0.scoreDescription, + placeholder: $0.placeholder + ) + } + ), + at: groupIndex + 1 ) + objectWillChange.send() } func updateTextField(area: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, text: String) { @@ -54,7 +67,12 @@ extension GSMAuthenticationFormModel: GSMAuthenticationFormActionProtocol { .select(selectedValue: select, values: uiModel.areas[area].sections[sectionIndex].groups[groupIndex].fields[fieldIndex].findFieldTypeValue()) } - func deleteField(area: Int, sectionIndex: Int, groupIndex: Int) { - uiModel.areas[area].sections[sectionIndex].groups[groupIndex].fields.remove(at: uiModel.areas[area].sections[sectionIndex].groups[groupIndex].fields.endIndex - 1) + func deleteField( + area: Int, + sectionIndex: Int, + groupIndex: Int + ) { + uiModel.areas[area].sections[sectionIndex].groups.remove(at: groupIndex) + objectWillChange.send() } } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift index 8eae7a04..fd0f8db4 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift @@ -1,16 +1,19 @@ +import AuthenticationDomainInterface import Foundation + protocol GSMAuthenticationFormStateProtocol { var uiModel: GSMAuthenticationFormUIModel { get } + var authenticationEntity: AuthenticationFormEntity? { get } } protocol GSMAuthenticationFormActionProtocol: AnyObject { func updateGSMAuthenticationFormUIModel(uiModel: GSMAuthenticationFormUIModel) + func updateAuthenticationEntity(authenticationEntity: AuthenticationFormEntity) func appendField( area: Int, sectionIndex: Int, - groupIndex: Int, - fields: [GSMAuthenticationFormUIModel.Area.Section.Group.Field] + groupIndex: Int ) func updateTextField( diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift index 8b5e295b..183a1a00 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift @@ -33,6 +33,16 @@ struct GSMAuthenticationFormUIModel { case boolean(selectedValue: String?, values: [String]) case file(fileName: String?) case select(selectedValue: String?, values: [String]) + + var emptyValue: FieldType { + switch self { + case .text: return .text(value: nil) + case .number: return .number(value: nil) + case let .boolean(_, values): return .boolean(selectedValue: nil, values: values) + case .file: return .file(fileName: nil) + case let .select(_, values): return .select(selectedValue: nil, values: values) + } + } } func findFieldTypeValue() -> [String] { diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift index be78f4db..02c3cb5c 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift @@ -14,7 +14,8 @@ enum FieldChanges { enum FieldInteraction { case fieldChanges(area: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, fieldChanges: FieldChanges) - case fieldAdd(area: Int, section: Int, field: Int) + case groupAdd(area: Int, section: Int, group: Int) + case groupRemove(area: Int, section: Int, group: Int) } struct GSMAuthenticationFormBuilderView: View { @@ -92,7 +93,7 @@ struct GSMAuthenticationFormBuilderView: View { @ViewBuilder private func groupList(areaIndex: Int, sectionIndex: Int, groups: [Group], maxCount: Int) -> some View { - LazyVStack(spacing: 0) { + LazyVStack(spacing: 16) { ForEach(groups.indices, id: \.self) { index in VStack { fieldList( @@ -103,25 +104,20 @@ struct GSMAuthenticationFormBuilderView: View { ) HStack { - ConditionView(maxCount > groups[index].fields.count) { + ConditionView(maxCount > groups.count && maxCount > 1) { SMSChip("추가") { - intent.appendField( - area: areaIndex, - sectionIndex: sectionIndex, - groupIndex: index, - fields: groups[index].fields + onFieldInteraction( + .groupAdd(area: areaIndex, section: sectionIndex, group: index) ) } } Spacer() - ConditionView(groups[index].fields.count > 1 && maxCount > 1) { + ConditionView(groups.count > maxCount && maxCount > 1) { Button { - intent.deleteField( - area: areaIndex, - sectionIndex: sectionIndex, - groupIndex: index + onFieldInteraction( + .groupRemove(area: areaIndex, section: sectionIndex, group: index) ) } label: { SMSIcon(.trash) diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift index d6a23c58..300ef6ba 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift @@ -38,8 +38,10 @@ struct GSMAuthenticationFormView: View { case let .select(select): intent.updateSelectField(area: area, sectionIndex: section, groupIndex: group, fieldIndex: field, select: select) } - case let .fieldAdd(area, section, field): - break + case let .groupAdd(area, section, group): + intent.appendField(area: area, sectionIndex: section, groupIndex: group) + case let .groupRemove(area, section, group): + intent.deleteField(area: area, sectionIndex: section, groupIndex: group) } } .padding(.bottom, safeAreaInsets.bottom + 16) From cb3b68f5f79d539976bfe4946982b24ed41399ca Mon Sep 17 00:00:00 2001 From: baegteun Date: Sat, 6 Jul 2024 20:34:55 +0900 Subject: [PATCH 04/13] =?UTF-8?q?:recycle:=20::=20[#342]=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EB=B2=84=ED=8A=BC=20=ED=91=9C=ED=98=84=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20maxCount=20>=201=EB=A7=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scene/Components/GSMAuthenticationFormBuilderView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift index 02c3cb5c..1dd2bb12 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift @@ -114,7 +114,7 @@ struct GSMAuthenticationFormBuilderView: View { Spacer() - ConditionView(groups.count > maxCount && maxCount > 1) { + ConditionView(maxCount > 1) { Button { onFieldInteraction( .groupRemove(area: areaIndex, section: sectionIndex, group: index) From 5395158b519dd224c1fbfbfc8ae5aaed2bba17ca Mon Sep 17 00:00:00 2001 From: baegteun Date: Sat, 6 Jul 2024 20:54:01 +0900 Subject: [PATCH 05/13] :pencil2: :: CodingKey --- .../Request/InputAuthenticationRequestDTO.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Projects/Domain/AuthenticationDomain/Interface/DTO/Request/InputAuthenticationRequestDTO.swift b/Projects/Domain/AuthenticationDomain/Interface/DTO/Request/InputAuthenticationRequestDTO.swift index 3b981222..aca61824 100644 --- a/Projects/Domain/AuthenticationDomain/Interface/DTO/Request/InputAuthenticationRequestDTO.swift +++ b/Projects/Domain/AuthenticationDomain/Interface/DTO/Request/InputAuthenticationRequestDTO.swift @@ -16,6 +16,11 @@ public struct Content: Encodable { self.sectionID = sectionID self.objects = objects } + + enum CodingKeys: String, CodingKey { + case sectionID = "sectionId" + case objects + } } public struct Object: Encodable { @@ -26,6 +31,11 @@ public struct Object: Encodable { self.groupId = groupId self.fields = fields } + + enum CodingKeys: String, CodingKey { + case groupId = "groupId" + case fields + } } // MARK: - Object @@ -41,4 +51,11 @@ public struct Field: Encodable { self.value = value self.selectID = selectID } + + enum CodingKeys: String, CodingKey { + case fieldID = "fieldId" + case fieldType + case value + case selectID = "selectId" + } } From dbb6f3822c0300b4be367a26c54a8e64bbe8a922 Mon Sep 17 00:00:00 2001 From: baegteun Date: Sun, 7 Jul 2024 00:08:38 +0900 Subject: [PATCH 06/13] =?UTF-8?q?:recycle:=20::=20Authentication=EC=97=90?= =?UTF-8?q?=20=EC=9D=98=EC=A1=B4=ED=95=B4=EC=84=9C=20=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model/GSMAuthenticationFormModel.swift | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift index 4c8c57b2..96d9c2a3 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift @@ -26,23 +26,36 @@ extension GSMAuthenticationFormModel: GSMAuthenticationFormActionProtocol { sectionIndex: Int, groupIndex: Int ) { - let selectedGroup: GSMAuthenticationFormUIModel.Area.Section.Group = uiModel.areas[area].sections[sectionIndex].groups[groupIndex] + guard let authenticationEntity else { return } + assert( + authenticationEntity.areas[area] + .sections[sectionIndex] + .groups.count == 1, + "section : group = 1 : 1 관계인 정책" + ) + guard let selectedGroup = authenticationEntity.areas[area] + .sections[sectionIndex] + .groups + .first + else { return } + + let fields = selectedGroup.fields.map { + GSMAuthenticationFormUIModel.Area.Section.Group.Field.init( + fieldId: $0.fieldId, + type: toUIFieldType(field: $0), + scoreDescription: $0.scoreDescription, + placeholder: $0.placeholder + ) + } + uiModel.areas[area].sections[sectionIndex].groups.insert( .init( groupId: selectedGroup.groupId, maxScore: selectedGroup.maxScore, - fields: selectedGroup.fields.map { - GSMAuthenticationFormUIModel.Area.Section.Group.Field.init( - fieldId: $0.fieldId, - type: $0.type.emptyValue, - scoreDescription: $0.scoreDescription, - placeholder: $0.placeholder - ) - } + fields: fields ), - at: groupIndex + 1 + at: groupIndex ) - objectWillChange.send() } func updateTextField(area: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, text: String) { @@ -73,6 +86,19 @@ extension GSMAuthenticationFormModel: GSMAuthenticationFormActionProtocol { groupIndex: Int ) { uiModel.areas[area].sections[sectionIndex].groups.remove(at: groupIndex) - objectWillChange.send() + } +} + +private extension GSMAuthenticationFormModel { + func toUIFieldType( + field: FieldEntity + ) -> GSMAuthenticationFormUIModel.Area.Section.Group.Field.FieldType { + switch field.type { + case .text: return .text(value: nil) + case .number: return .number(value: nil) + case .boolean: return .boolean(selectedValue: nil, values: field.values?.map { $0.value } ?? []) + case .file: return .file(fileName: nil) + case .select: return .select(selectedValue: nil, values: field.values?.map { $0.value } ?? []) + } } } From bb0fff7c4762164a011d6e060aeb75b0c99a2367 Mon Sep 17 00:00:00 2001 From: baegteun Date: Mon, 8 Jul 2024 22:08:01 +0900 Subject: [PATCH 07/13] :recycle: :: [#342] DTO Field Name --- .../Sources/DTO/Response/ContentsResponseDTO.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Projects/Domain/AuthenticationDomain/Sources/DTO/Response/ContentsResponseDTO.swift b/Projects/Domain/AuthenticationDomain/Sources/DTO/Response/ContentsResponseDTO.swift index 1ba8dfd5..961af9b9 100644 --- a/Projects/Domain/AuthenticationDomain/Sources/DTO/Response/ContentsResponseDTO.swift +++ b/Projects/Domain/AuthenticationDomain/Sources/DTO/Response/ContentsResponseDTO.swift @@ -39,6 +39,11 @@ public struct ContentsResponseDTO: Decodable { public let scoreDescription: String? public let values: [ValuesResponseDTO]? public let example: String? + + enum CodingKeys: String, CodingKey { + case fieldId, fieldType, scoreDescription, values + case example = "placeholder" + } } public struct ValuesResponseDTO: Decodable { From 506e37a096490bcc1f2c6fac899f98903054630e Mon Sep 17 00:00:00 2001 From: Sunghun Kim <81547954+kimsh153@users.noreply.github.com> Date: Mon, 8 Jul 2024 22:44:32 +0900 Subject: [PATCH 08/13] =?UTF-8?q?:recycle:=20::=20trash=20button=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=EB=AC=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/GSMAuthenticationFormBuilderView.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift index 1dd2bb12..d2005be3 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift @@ -114,7 +114,7 @@ struct GSMAuthenticationFormBuilderView: View { Spacer() - ConditionView(maxCount > 1) { + ConditionView(groups.count > 1 && maxCount > 1) { Button { onFieldInteraction( .groupRemove(area: areaIndex, section: sectionIndex, group: index) @@ -151,7 +151,13 @@ struct GSMAuthenticationFormBuilderView: View { } @ViewBuilder - private func fieldView(areaIndex: Int, sectionIndex: Int, groupIndex: Int, fieldIndex: Int, field: Field) -> some View { + private func fieldView( + areaIndex: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + field: Field + ) -> some View { switch field.type { case let .text(value): textTypeFieldView( From a30a2e772ae8590ecab47bc9a0d0a76f1c4a200b Mon Sep 17 00:00:00 2001 From: Sunghun Kim <81547954+kimsh153@users.noreply.github.com> Date: Tue, 9 Jul 2024 22:43:36 +0900 Subject: [PATCH 09/13] =?UTF-8?q?:sparkles:=20::=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=A0=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Application/NeedleGenerated.swift | 60 ++++++++++--------- .../Sources/File/SMSFileField.swift | 4 -- .../Interface/DI/FileDomainBuildable.swift | 1 + .../DataSource/RemoteFileDataSource.swift | 1 + .../Interface/Error/FileDomainError.swift | 4 ++ .../Interface/Repository/FileRepository.swift | 1 + .../Interface/UseCase/FileUploadUseCase.swift | 5 ++ .../Sources/DI/FileDomainComponent.swift | 3 + .../DTO/Response/FileUploadResponseDTO.swift | 9 +++ .../DataSource/RemoteFileDataSourceImpl.swift | 7 +++ .../Sources/Endpoint/FileEndpoint.swift | 17 ++++++ .../Repository/FileRepositoryImpl.swift | 4 ++ .../UseCase/FileUploadUseCaseImpl.swift | 14 +++++ .../Project.swift | 3 +- .../DI/GSMAuthenticationComponent.swift | 5 +- .../Intent/GSMAuthenticationFormIntent.swift | 15 ++++- .../GSMAuthenticationFormIntentProtocol.swift | 4 +- .../GSMAuthenticationFormBuilderView.swift | 21 ++++--- .../Scene/GSMAuthenticationFormView.swift | 4 +- 19 files changed, 133 insertions(+), 49 deletions(-) create mode 100644 Projects/Domain/FileDomain/Interface/UseCase/FileUploadUseCase.swift create mode 100644 Projects/Domain/FileDomain/Sources/DTO/Response/FileUploadResponseDTO.swift create mode 100644 Projects/Domain/FileDomain/Sources/UseCase/FileUploadUseCaseImpl.swift diff --git a/Projects/App/Sources/Application/NeedleGenerated.swift b/Projects/App/Sources/Application/NeedleGenerated.swift index 2716594e..04504c8d 100644 --- a/Projects/App/Sources/Application/NeedleGenerated.swift +++ b/Projects/App/Sources/Application/NeedleGenerated.swift @@ -92,6 +92,9 @@ private class GSMAuthenticationDependencydf257bda3051cc91534fProvider: GSMAuthen var authenticationDomainBuildable: any AuthenticationDomainBuildable { return appComponent.authenticationDomainBuildable } + var fileDomainBuildable: any FileDomainBuildable { + return appComponent.fileDomainBuildable + } private let appComponent: AppComponent init(appComponent: AppComponent) { self.appComponent = appComponent @@ -471,33 +474,33 @@ extension JwtStoreComponent: Registration { extension AppComponent: Registration { public func registerItems() { - localTable["rootComponent-RootComponent"] = { [unowned self] in self.rootComponent as Any } - localTable["signinBuildable-any SigninBuildable"] = { [unowned self] in self.signinBuildable as Any } - localTable["inputInformationBuildable-any InputInformationBuildable"] = { [unowned self] in self.inputInformationBuildable as Any } - localTable["inputProfileInfoBuildable-any InputProfileInfoBuildable"] = { [unowned self] in self.inputProfileInfoBuildable as Any } - localTable["inputSchoolLifeInfoBuildable-any InputSchoolLifeInfoBuildable"] = { [unowned self] in self.inputSchoolLifeInfoBuildable as Any } - localTable["inputWorkInfoBuildable-any InputWorkInfoBuildable"] = { [unowned self] in self.inputWorkInfoBuildable as Any } - localTable["inputMilitaryInfoBuildable-any InputMilitaryInfoBuildable"] = { [unowned self] in self.inputMilitaryInfoBuildable as Any } - localTable["inputCertificateInfoBuildable-any InputCertificateInfoBuildable"] = { [unowned self] in self.inputCertificateInfoBuildable as Any } - localTable["inputLanguageInfoBuildable-any InputLanguageInfoBuildable"] = { [unowned self] in self.inputLanguageInfoBuildable as Any } - localTable["inputPrizeInfoBuildable-any InputPrizeInfoBuildable"] = { [unowned self] in self.inputPrizeInfoBuildable as Any } - localTable["inputProjectInfoBuildable-any InputProjectInfoBuildable"] = { [unowned self] in self.inputProjectInfoBuildable as Any } - localTable["mainBuildable-any MainBuildable"] = { [unowned self] in self.mainBuildable as Any } - localTable["myPageBuildable-any MyPageBuildable"] = { [unowned self] in self.myPageBuildable as Any } - localTable["techStackAppendBuildable-any TechStackAppendBuildable"] = { [unowned self] in self.techStackAppendBuildable as Any } - localTable["studentDetailBuildable-any StudentDetailBuildable"] = { [unowned self] in self.studentDetailBuildable as Any } - localTable["filterBuildable-any FilterBuildable"] = { [unowned self] in self.filterBuildable as Any } - localTable["splashBuildable-any SplashBuildable"] = { [unowned self] in self.splashBuildable as Any } - localTable["gsmAuthenticationBuildable-any GSMAuthenticationBuildable"] = { [unowned self] in self.gsmAuthenticationBuildable as Any } - localTable["authDomainBuildable-any AuthDomainBuildable"] = { [unowned self] in self.authDomainBuildable as Any } - localTable["studentDomainBuildable-any StudentDomainBuildable"] = { [unowned self] in self.studentDomainBuildable as Any } - localTable["majorDomainBuildable-any MajorDomainBuildable"] = { [unowned self] in self.majorDomainBuildable as Any } - localTable["fileDomainBuildable-any FileDomainBuildable"] = { [unowned self] in self.fileDomainBuildable as Any } - localTable["userDomainBuildable-any UserDomainBuildable"] = { [unowned self] in self.userDomainBuildable as Any } - localTable["techStackDomainBuildable-any TechStackDomainBuildable"] = { [unowned self] in self.techStackDomainBuildable as Any } - localTable["jwtStoreBuildable-any JwtStoreBuildable"] = { [unowned self] in self.jwtStoreBuildable as Any } - localTable["keychainBuildable-any KeychainBuildable"] = { [unowned self] in self.keychainBuildable as Any } - localTable["authenticationDomainBuildable-any AuthenticationDomainBuildable"] = { [unowned self] in self.authenticationDomainBuildable as Any } + localTable["rootComponent-RootComponent"] = { self.rootComponent as Any } + localTable["signinBuildable-any SigninBuildable"] = { self.signinBuildable as Any } + localTable["inputInformationBuildable-any InputInformationBuildable"] = { self.inputInformationBuildable as Any } + localTable["inputProfileInfoBuildable-any InputProfileInfoBuildable"] = { self.inputProfileInfoBuildable as Any } + localTable["inputSchoolLifeInfoBuildable-any InputSchoolLifeInfoBuildable"] = { self.inputSchoolLifeInfoBuildable as Any } + localTable["inputWorkInfoBuildable-any InputWorkInfoBuildable"] = { self.inputWorkInfoBuildable as Any } + localTable["inputMilitaryInfoBuildable-any InputMilitaryInfoBuildable"] = { self.inputMilitaryInfoBuildable as Any } + localTable["inputCertificateInfoBuildable-any InputCertificateInfoBuildable"] = { self.inputCertificateInfoBuildable as Any } + localTable["inputLanguageInfoBuildable-any InputLanguageInfoBuildable"] = { self.inputLanguageInfoBuildable as Any } + localTable["inputPrizeInfoBuildable-any InputPrizeInfoBuildable"] = { self.inputPrizeInfoBuildable as Any } + localTable["inputProjectInfoBuildable-any InputProjectInfoBuildable"] = { self.inputProjectInfoBuildable as Any } + localTable["mainBuildable-any MainBuildable"] = { self.mainBuildable as Any } + localTable["myPageBuildable-any MyPageBuildable"] = { self.myPageBuildable as Any } + localTable["techStackAppendBuildable-any TechStackAppendBuildable"] = { self.techStackAppendBuildable as Any } + localTable["studentDetailBuildable-any StudentDetailBuildable"] = { self.studentDetailBuildable as Any } + localTable["filterBuildable-any FilterBuildable"] = { self.filterBuildable as Any } + localTable["splashBuildable-any SplashBuildable"] = { self.splashBuildable as Any } + localTable["gsmAuthenticationBuildable-any GSMAuthenticationBuildable"] = { self.gsmAuthenticationBuildable as Any } + localTable["authDomainBuildable-any AuthDomainBuildable"] = { self.authDomainBuildable as Any } + localTable["studentDomainBuildable-any StudentDomainBuildable"] = { self.studentDomainBuildable as Any } + localTable["majorDomainBuildable-any MajorDomainBuildable"] = { self.majorDomainBuildable as Any } + localTable["fileDomainBuildable-any FileDomainBuildable"] = { self.fileDomainBuildable as Any } + localTable["userDomainBuildable-any UserDomainBuildable"] = { self.userDomainBuildable as Any } + localTable["techStackDomainBuildable-any TechStackDomainBuildable"] = { self.techStackDomainBuildable as Any } + localTable["jwtStoreBuildable-any JwtStoreBuildable"] = { self.jwtStoreBuildable as Any } + localTable["keychainBuildable-any KeychainBuildable"] = { self.keychainBuildable as Any } + localTable["authenticationDomainBuildable-any AuthenticationDomainBuildable"] = { self.authenticationDomainBuildable as Any } } } extension KeychainComponent: Registration { @@ -508,6 +511,7 @@ extension KeychainComponent: Registration { extension GSMAuthenticationComponent: Registration { public func registerItems() { keyPathToName[\GSMAuthenticationDependency.authenticationDomainBuildable] = "authenticationDomainBuildable-any AuthenticationDomainBuildable" + keyPathToName[\GSMAuthenticationDependency.fileDomainBuildable] = "fileDomainBuildable-any FileDomainBuildable" } } extension SplashComponent: Registration { @@ -665,7 +669,7 @@ private func registerProviderFactory(_ componentPath: String, _ factory: @escapi #if !NEEDLE_DYNAMIC -@inline(never) private func register1() { +private func register1() { registerProviderFactory("^->AppComponent->JwtStoreComponent", factoryb27d5aae1eb7e73575a6f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent", factoryEmptyDependencyProvider) registerProviderFactory("^->AppComponent->KeychainComponent", factoryEmptyDependencyProvider) diff --git a/Projects/Core/DesignSystem/Sources/File/SMSFileField.swift b/Projects/Core/DesignSystem/Sources/File/SMSFileField.swift index f02b6442..9132ed12 100644 --- a/Projects/Core/DesignSystem/Sources/File/SMSFileField.swift +++ b/Projects/Core/DesignSystem/Sources/File/SMSFileField.swift @@ -39,10 +39,6 @@ public struct SMSFileField: View { isOnClear: false ) .disabled(true) - .overlay(alignment: .trailing) { - SMSIcon(.folder) - .padding(.trailing, 12) - } .simultaneousGesture( TapGesture() .onEnded { diff --git a/Projects/Domain/FileDomain/Interface/DI/FileDomainBuildable.swift b/Projects/Domain/FileDomain/Interface/DI/FileDomainBuildable.swift index f92da64a..b3332d42 100644 --- a/Projects/Domain/FileDomain/Interface/DI/FileDomainBuildable.swift +++ b/Projects/Domain/FileDomain/Interface/DI/FileDomainBuildable.swift @@ -1,4 +1,5 @@ public protocol FileDomainBuildable { var imageUploadUseCase: any ImageUploadUseCase { get } + var fileUploadUseCase: any FileUploadUseCase { get } var fileRepository: any FileRepository { get } } diff --git a/Projects/Domain/FileDomain/Interface/DataSource/RemoteFileDataSource.swift b/Projects/Domain/FileDomain/Interface/DataSource/RemoteFileDataSource.swift index d1a6cbe5..d0f6de6c 100644 --- a/Projects/Domain/FileDomain/Interface/DataSource/RemoteFileDataSource.swift +++ b/Projects/Domain/FileDomain/Interface/DataSource/RemoteFileDataSource.swift @@ -2,4 +2,5 @@ import Foundation public protocol RemoteFileDataSource { func imageUpload(image: Data, fileName: String) async throws -> String + func fileUpload(file: Data, fileName: String) async throws -> String } diff --git a/Projects/Domain/FileDomain/Interface/Error/FileDomainError.swift b/Projects/Domain/FileDomain/Interface/Error/FileDomainError.swift index 6108e650..7e51bbea 100644 --- a/Projects/Domain/FileDomain/Interface/Error/FileDomainError.swift +++ b/Projects/Domain/FileDomain/Interface/Error/FileDomainError.swift @@ -2,6 +2,7 @@ import Foundation public enum FileDomainError: Error { case notImageType + case notFileType case internalServerError } @@ -11,6 +12,9 @@ extension FileDomainError: LocalizedError { case .notImageType: return "이미지 형식이 jpg, jpeg, png, heic인 이미지가 아닙니다." + case .notFileType: + return "파일 형식이 hw, hwpx, png인 파일이 아닙니다." + case .internalServerError: return "서버에서 문제가 발생하였습니다. 지속될 시 문의해주세요." } diff --git a/Projects/Domain/FileDomain/Interface/Repository/FileRepository.swift b/Projects/Domain/FileDomain/Interface/Repository/FileRepository.swift index 913b8175..2dca6179 100644 --- a/Projects/Domain/FileDomain/Interface/Repository/FileRepository.swift +++ b/Projects/Domain/FileDomain/Interface/Repository/FileRepository.swift @@ -2,4 +2,5 @@ import Foundation public protocol FileRepository { func imageUpload(image: Data, fileName: String) async throws -> String + func fileUpload(file: Data, fileName: String) async throws -> String } diff --git a/Projects/Domain/FileDomain/Interface/UseCase/FileUploadUseCase.swift b/Projects/Domain/FileDomain/Interface/UseCase/FileUploadUseCase.swift new file mode 100644 index 00000000..4df39a91 --- /dev/null +++ b/Projects/Domain/FileDomain/Interface/UseCase/FileUploadUseCase.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol FileUploadUseCase { + func execute(file: Data, fileName: String) async throws -> String +} diff --git a/Projects/Domain/FileDomain/Sources/DI/FileDomainComponent.swift b/Projects/Domain/FileDomain/Sources/DI/FileDomainComponent.swift index c8aa1dbb..e7940634 100644 --- a/Projects/Domain/FileDomain/Sources/DI/FileDomainComponent.swift +++ b/Projects/Domain/FileDomain/Sources/DI/FileDomainComponent.swift @@ -7,6 +7,9 @@ public protocol FileDomainDependency: Dependency { } public final class FileDomainComponent: Component, FileDomainBuildable { + public var fileUploadUseCase: any FileUploadUseCase { + FileUploadUseCaseImpl(fileRepository: fileRepository) + } public var imageUploadUseCase: any ImageUploadUseCase { ImageUploadUseCaseImpl(fileRepository: fileRepository) } diff --git a/Projects/Domain/FileDomain/Sources/DTO/Response/FileUploadResponseDTO.swift b/Projects/Domain/FileDomain/Sources/DTO/Response/FileUploadResponseDTO.swift new file mode 100644 index 00000000..38bbfcc8 --- /dev/null +++ b/Projects/Domain/FileDomain/Sources/DTO/Response/FileUploadResponseDTO.swift @@ -0,0 +1,9 @@ +import Foundation + +public struct FileUploadResponseDTO: Decodable { + public let fileURL: String + + enum CodingKeys: String, CodingKey { + case fileURL = "fileUrl" + } +} diff --git a/Projects/Domain/FileDomain/Sources/DataSource/RemoteFileDataSourceImpl.swift b/Projects/Domain/FileDomain/Sources/DataSource/RemoteFileDataSourceImpl.swift index 687b87de..136d4af1 100644 --- a/Projects/Domain/FileDomain/Sources/DataSource/RemoteFileDataSourceImpl.swift +++ b/Projects/Domain/FileDomain/Sources/DataSource/RemoteFileDataSourceImpl.swift @@ -10,4 +10,11 @@ final class RemoteFileDataSourceImpl: BaseRemoteDataSource, Remote ) .fileURL } + + func fileUpload(file: Data, fileName: String) async throws -> String { + try await request( + .fileUpload(file: file, fileName: fileName), + dto: FileUploadResponseDTO.self + ).fileURL + } } diff --git a/Projects/Domain/FileDomain/Sources/Endpoint/FileEndpoint.swift b/Projects/Domain/FileDomain/Sources/Endpoint/FileEndpoint.swift index 8e23daf0..fc42feb9 100644 --- a/Projects/Domain/FileDomain/Sources/Endpoint/FileEndpoint.swift +++ b/Projects/Domain/FileDomain/Sources/Endpoint/FileEndpoint.swift @@ -5,6 +5,7 @@ import Foundation enum FileEndpoint { case imageUpload(image: Data, fileName: String) + case fileUpload(file: Data, fileName: String) } extension FileEndpoint: SMSEndpoint { @@ -18,6 +19,9 @@ extension FileEndpoint: SMSEndpoint { switch self { case .imageUpload: return .post("/image") + + case .fileUpload: + return .post("") } } @@ -28,6 +32,11 @@ extension FileEndpoint: SMSEndpoint { MultiPartFormData(field: "file", data: image, fileName: fileName) ]) + case let .fileUpload(file, fileName): + return .uploadMultipart([ + MultiPartFormData(field: "file", data: file, fileName: fileName) + ]) + default: return .requestPlain } @@ -38,6 +47,9 @@ extension FileEndpoint: SMSEndpoint { case .imageUpload: return .accessToken + case .fileUpload: + return .accessToken + default: return .none } @@ -54,6 +66,11 @@ extension FileEndpoint: SMSEndpoint { 400: .notImageType, 500: .internalServerError ] + case .fileUpload: + return [ + 400: .notFileType, + 500: .internalServerError + ] } } } diff --git a/Projects/Domain/FileDomain/Sources/Repository/FileRepositoryImpl.swift b/Projects/Domain/FileDomain/Sources/Repository/FileRepositoryImpl.swift index 75172fac..62014a44 100644 --- a/Projects/Domain/FileDomain/Sources/Repository/FileRepositoryImpl.swift +++ b/Projects/Domain/FileDomain/Sources/Repository/FileRepositoryImpl.swift @@ -11,4 +11,8 @@ struct FileRepositoryImpl: FileRepository { func imageUpload(image: Data, fileName: String) async throws -> String { try await remoteFileDataSource.imageUpload(image: image, fileName: fileName) } + + func fileUpload(file: Data, fileName: String) async throws -> String { + try await remoteFileDataSource.fileUpload(file: file, fileName: fileName) + } } diff --git a/Projects/Domain/FileDomain/Sources/UseCase/FileUploadUseCaseImpl.swift b/Projects/Domain/FileDomain/Sources/UseCase/FileUploadUseCaseImpl.swift new file mode 100644 index 00000000..72a76c2b --- /dev/null +++ b/Projects/Domain/FileDomain/Sources/UseCase/FileUploadUseCaseImpl.swift @@ -0,0 +1,14 @@ +import FileDomainInterface +import Foundation + +struct FileUploadUseCaseImpl: FileUploadUseCase { + private let fileRepository: any FileRepository + + init(fileRepository: any FileRepository) { + self.fileRepository = fileRepository + } + + func execute(file: Data, fileName: String) async throws -> String { + try await fileRepository.fileUpload(file: file, fileName: fileName) + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Project.swift b/Projects/Feature/GSMAuthenticationFormFeature/Project.swift index 727e6169..5418184b 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Project.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Project.swift @@ -8,7 +8,8 @@ let project = Project.makeModule( targets: [.interface, .unitTest, .demo], internalDependencies: [ .Feature.BaseFeature, - .Domain.AuthenticationDomainInterface + .Domain.AuthenticationDomainInterface, + .Domain.FileDomainInterface ], demoDependencies: [ .Domain.AuthenticationDomainTesting diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/DI/GSMAuthenticationComponent.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/DI/GSMAuthenticationComponent.swift index 4e140e80..fa417ff3 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/DI/GSMAuthenticationComponent.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/DI/GSMAuthenticationComponent.swift @@ -4,9 +4,11 @@ import NeedleFoundation import SwiftUI import GSMAuthenticationFormFeatureInterface import AuthenticationDomainInterface +import FileDomainInterface public protocol GSMAuthenticationDependency: Dependency { var authenticationDomainBuildable: any AuthenticationDomainBuildable { get } + var fileDomainBuildable: any FileDomainBuildable { get } } public final class GSMAuthenticationComponent: Component, GSMAuthenticationBuildable { @@ -15,7 +17,8 @@ public final class GSMAuthenticationComponent: Component Date: Tue, 9 Jul 2024 22:49:58 +0900 Subject: [PATCH 10/13] =?UTF-8?q?:lipstick:=20::=20[#342]=20Collapse=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/FormBuilderAreaListView.swift | 41 ++ .../Components/FormBuilderAreaView.swift | 352 ++++++++++++++++++ .../GSMAuthenticationFormBuilderView.swift | 347 +---------------- 3 files changed, 395 insertions(+), 345 deletions(-) create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/FormBuilderAreaListView.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/FormBuilderAreaView.swift diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/FormBuilderAreaListView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/FormBuilderAreaListView.swift new file mode 100644 index 00000000..ae1784a2 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/FormBuilderAreaListView.swift @@ -0,0 +1,41 @@ +import DesignSystem +import SwiftUI +import ViewUtil + +struct FormBuilderAreaListView: View { + private typealias Area = GSMAuthenticationFormUIModel.Area + private typealias Section = Area.Section + private typealias Group = Section.Group + private typealias Field = Group.Field + + @Environment(\.fileFieldPresenter) var fileFieldPresenter + @Environment(\.optionPickerPresenter) var optionPickerPresenter + @State private var collapseList: [Bool] = [] + let areas: [GSMAuthenticationFormUIModel.Area] + let onFieldInteraction: (FieldInteraction) -> Void + + init(areas: [GSMAuthenticationFormUIModel.Area], onFieldInteraction: @escaping (FieldInteraction) -> Void) { + self.areas = areas + self.onFieldInteraction = onFieldInteraction + } + + var body: some View { + areaList(areas: areas) + .onAppear { + collapseList = Array(repeating: false, count: areas.count) + } + } + + @ViewBuilder + private func areaList(areas: [Area]) -> some View { + LazyVStack(spacing: 16) { + ForEach(areas.indices, id: \.self) { index in + SMSSeparator() + .padding(.bottom, 4) + + FormBuilderAreaView(index: index, area: areas[index], onFieldInteraction: onFieldInteraction) + } + } + + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/FormBuilderAreaView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/FormBuilderAreaView.swift new file mode 100644 index 00000000..df356b83 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/FormBuilderAreaView.swift @@ -0,0 +1,352 @@ +import DesignSystem +import SwiftUI +import ViewUtil + +struct FormBuilderAreaView: View { + private typealias Area = GSMAuthenticationFormUIModel.Area + private typealias Section = Area.Section + private typealias Group = Section.Group + private typealias Field = Group.Field + + @Environment(\.fileFieldPresenter) var fileFieldPresenter + @Environment(\.optionPickerPresenter) var optionPickerPresenter + @State private var isCollapsed = false + let index: Int + let area: GSMAuthenticationFormUIModel.Area + let onFieldInteraction: (FieldInteraction) -> Void + + var body: some View { + HStack(spacing: 16) { + SMSText(area.title, font: .title1) + + Spacer() + + SMSIcon(.downChevron) + .rotationEffect(isCollapsed == false ? .degrees(90) : .degrees(0)) + .buttonWrapper { + isCollapsed.toggle() + } + } + .padding(.horizontal, 20) + + if isCollapsed == false { + sectionList(areaIndex: index, sections: area.sections) + .padding(.horizontal, 20) + } + } + + @ViewBuilder + private func sectionList(areaIndex: Int, sections: [Section]) -> some View { + LazyVStack(spacing: 0) { + ForEach(sections.indices, id: \.self) { index in + VStack { + groupList(areaIndex: areaIndex, sectionIndex: index, groups: sections[index].groups, maxCount: sections[index].maxCount) + } + .titleWrapper(sections[index].title) + .frame(maxWidth: .infinity) + } + .padding(.vertical, 20) + } + } + + @ViewBuilder + private func groupList(areaIndex: Int, sectionIndex: Int, groups: [Group], maxCount: Int) -> some View { + LazyVStack(spacing: 16) { + ForEach(groups.indices, id: \.self) { index in + VStack { + fieldList( + areaIndex: areaIndex, + sectionIndex: sectionIndex, + groupIndex: index, + fields: groups[index].fields + ) + + HStack { + ConditionView(maxCount > groups.count && maxCount > 1) { + SMSChip("추가") { + onFieldInteraction( + .groupAdd(area: areaIndex, section: sectionIndex, group: index) + ) + } + } + + Spacer() + + ConditionView(groups.count > 1 && maxCount > 1) { + Button { + onFieldInteraction( + .groupRemove(area: areaIndex, section: sectionIndex, group: index) + ) + } label: { + SMSIcon(.trash) + } + } + } + } + } + } + } + + @ViewBuilder + private func fieldList(areaIndex: Int, sectionIndex: Int, groupIndex: Int, fields: [Field]) -> some View { + LazyVStack(spacing: 0) { + ForEach(fields.indices, id: \.self) { index in + fieldView( + areaIndex: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: index, + field: fields[index] + ) + .titleWrapper( + fields[index].scoreDescription ?? "", + position: .bottom(.leading), + font: .caption1, + color: .neutral(.n30) + ) + } + } + } + + @ViewBuilder + private func fieldView( + areaIndex: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + field: Field + ) -> some View { + switch field.type { + case let .text(value): + textTypeFieldView( + areaIndex: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + placeholder: field.placeholder, + text: value + ) + + case let .number(value): + numberTypeFieldView( + areaIndex: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + placeholder: field.placeholder, + number: value + ) + + case let .boolean(selectedValue, values): + booleanTypeFieldView( + areaIndex: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + selectedValue: selectedValue, + values: values + ) + + case let .file(fileName): + fileTypeFieldView( + areaIndex: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + placeholder: field.placeholder, + fileName: fileName + ) + + case let .select(selectedValue, values): + selectTypeFieldView( + areaIndex: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + placeholder: field.placeholder, + selectedValue: selectedValue, + values: values + ) + } + } + + @ViewBuilder + private func textTypeFieldView( + areaIndex: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + placeholder: String?, + text: String? + ) -> some View { + SMSTextField( + placeholder ?? "", + text: Binding( + get: { text ?? "" }, + set: { newValue in + onFieldInteraction( + .fieldChanges( + area: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + fieldChanges: .text(newValue) + ) + ) + } + ) + ) + } + + @ViewBuilder + private func numberTypeFieldView( + areaIndex: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + placeholder: String?, + number: Int? + ) -> some View { + SMSTextField( + placeholder ?? "", + text: Binding( + get: { + if let number { + "\(number)" + } else { + "" + } + }, + set: { newValue in + guard let numberValue = Int(newValue) else { return } + onFieldInteraction( + .fieldChanges( + area: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + fieldChanges: .number(numberValue) + ) + ) + } + ) + ) + } + + @ViewBuilder + private func booleanTypeFieldView( + areaIndex: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + selectedValue: String?, + values: [String] + ) -> some View { + SMSSegmentedControl( + options: values, + selectedOption: selectedValue + ) { option in + switch option { + case values[0]: + onFieldInteraction( + .fieldChanges( + area: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + fieldChanges: .boolean(values[0]) + ) + ) + + case values[1]: + onFieldInteraction( + .fieldChanges( + area: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + fieldChanges: .boolean(values[1]) + ) + ) + + default: + return + } + } + } + + @ViewBuilder + private func fileTypeFieldView( + areaIndex: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + placeholder: String?, + fileName: String? + ) -> some View { + SMSFileField( + placeholder, + fileText: fileName + ) { result in + switch result { + case let .success(url): + onFieldInteraction( + .fieldChanges( + area: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + fieldChanges: .file(url.absoluteString) + ) + ) + + default: + return + } + } + } + + @ViewBuilder + private func selectTypeFieldView( + areaIndex: Int, + sectionIndex: Int, + groupIndex: Int, + fieldIndex: Int, + placeholder: String?, + selectedValue: String?, + values: [String] + ) -> some View { + SMSTextField( + placeholder ?? "", + text: Binding( + get: { selectedValue ?? "" }, + set: { _ in } + ), + isOnClear: false + ) + .disabled(true) + .overlay(alignment: .trailing) { + SMSIcon(.downChevron) + .padding(.trailing, 12) + } + .simultaneousGesture( + TapGesture() + .onEnded { + optionPickerPresenter.presentOptionPicker( + options: values, + onOptionSelect: { selectedOption in + onFieldInteraction( + .fieldChanges( + area: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + fieldChanges: .select(selectedOption) + ) + ) + } + ) + } + ) + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift index d2005be3..d1c05ed4 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift @@ -43,352 +43,9 @@ struct GSMAuthenticationFormBuilderView: View { ScrollView { GSMAuthenticationFileDownloadView(uiModel: uiModel.files) - areaList(areas: uiModel.areas) - } - } - - @ViewBuilder - private func areaList(areas: [Area]) -> some View { - LazyVStack(spacing: 16) { - ForEach(uiModel.areas.indices, id: \.self) { index in - SMSSeparator() - .padding(.bottom, 4) - - HStack(spacing: 16) { - SMSText(areas[index].title, font: .title1) - - Spacer() - - SMSIcon(.downChevron) - .rotationEffect(false ? .degrees(90) : .degrees(0)) - .buttonWrapper { - } - - SMSIcon(.xmarkOutline) - .buttonWrapper { - } - } - .padding(.horizontal, 20) - - sectionList(areaIndex: index, sections: areas[index].sections) - .padding(.horizontal, 20) - } - } - - } - - @ViewBuilder - private func sectionList(areaIndex: Int, sections: [Section]) -> some View { - LazyVStack(spacing: 0) { - ForEach(sections.indices, id: \.self) { index in - VStack { - groupList(areaIndex: areaIndex, sectionIndex: index, groups: sections[index].groups, maxCount: sections[index].maxCount) - } - .titleWrapper(sections[index].title) - .frame(maxWidth: .infinity) + FormBuilderAreaListView(areas: uiModel.areas) { fieldInteraction in + onFieldInteraction(fieldInteraction) } - .padding(.vertical, 20) - } - } - - @ViewBuilder - private func groupList(areaIndex: Int, sectionIndex: Int, groups: [Group], maxCount: Int) -> some View { - LazyVStack(spacing: 16) { - ForEach(groups.indices, id: \.self) { index in - VStack { - fieldList( - areaIndex: areaIndex, - sectionIndex: sectionIndex, - groupIndex: index, - fields: groups[index].fields - ) - - HStack { - ConditionView(maxCount > groups.count && maxCount > 1) { - SMSChip("추가") { - onFieldInteraction( - .groupAdd(area: areaIndex, section: sectionIndex, group: index) - ) - } - } - - Spacer() - - ConditionView(groups.count > 1 && maxCount > 1) { - Button { - onFieldInteraction( - .groupRemove(area: areaIndex, section: sectionIndex, group: index) - ) - } label: { - SMSIcon(.trash) - } - } - } - } - } - } - } - - @ViewBuilder - private func fieldList(areaIndex: Int, sectionIndex: Int, groupIndex: Int, fields: [Field]) -> some View { - LazyVStack(spacing: 0) { - ForEach(fields.indices, id: \.self) { index in - fieldView( - areaIndex: areaIndex, - sectionIndex: sectionIndex, - groupIndex: groupIndex, - fieldIndex: index, - field: fields[index] - ) - .titleWrapper( - fields[index].scoreDescription ?? "", - position: .bottom(.leading), - font: .caption1, - color: .neutral(.n30) - ) - } - } - } - - @ViewBuilder - private func fieldView( - areaIndex: Int, - sectionIndex: Int, - groupIndex: Int, - fieldIndex: Int, - field: Field - ) -> some View { - switch field.type { - case let .text(value): - textTypeFieldView( - areaIndex: areaIndex, - sectionIndex: sectionIndex, - groupIndex: groupIndex, - fieldIndex: fieldIndex, - placeholder: field.placeholder, - text: value - ) - - case let .number(value): - numberTypeFieldView( - areaIndex: areaIndex, - sectionIndex: sectionIndex, - groupIndex: groupIndex, - fieldIndex: fieldIndex, - placeholder: field.placeholder, - number: value - ) - - case let .boolean(selectedValue, values): - booleanTypeFieldView( - areaIndex: areaIndex, - sectionIndex: sectionIndex, - groupIndex: groupIndex, - fieldIndex: fieldIndex, - selectedValue: selectedValue, - values: values - ) - - case let .file(fileName): - fileTypeFieldView( - areaIndex: areaIndex, - sectionIndex: sectionIndex, - groupIndex: groupIndex, - fieldIndex: fieldIndex, - placeholder: field.placeholder, - fileName: fileName - ) - - case let .select(selectedValue, values): - selectTypeFieldView( - areaIndex: areaIndex, - sectionIndex: sectionIndex, - groupIndex: groupIndex, - fieldIndex: fieldIndex, - placeholder: field.placeholder, - selectedValue: selectedValue, - values: values - ) - } - } - - @ViewBuilder - private func textTypeFieldView( - areaIndex: Int, - sectionIndex: Int, - groupIndex: Int, - fieldIndex: Int, - placeholder: String?, - text: String? - ) -> some View { - SMSTextField( - placeholder ?? "", - text: Binding( - get: { text ?? "" }, - set: { newValue in - onFieldInteraction( - .fieldChanges( - area: areaIndex, - sectionIndex: sectionIndex, - groupIndex: groupIndex, - fieldIndex: fieldIndex, - fieldChanges: .text(newValue) - ) - ) - } - ) - ) - } - - @ViewBuilder - private func numberTypeFieldView( - areaIndex: Int, - sectionIndex: Int, - groupIndex: Int, - fieldIndex: Int, - placeholder: String?, - number: Int? - ) -> some View { - SMSTextField( - placeholder ?? "", - text: Binding( - get: { - if let number { - "\(number)" - } else { - "" - } - }, - set: { newValue in - guard let numberValue = Int(newValue) else { return } - onFieldInteraction( - .fieldChanges( - area: areaIndex, - sectionIndex: sectionIndex, - groupIndex: groupIndex, - fieldIndex: fieldIndex, - fieldChanges: .number(numberValue) - ) - ) - } - ) - ) - } - - @ViewBuilder - private func booleanTypeFieldView( - areaIndex: Int, - sectionIndex: Int, - groupIndex: Int, - fieldIndex: Int, - selectedValue: String?, - values: [String] - ) -> some View { - SMSSegmentedControl( - options: values, - selectedOption: selectedValue - ) { option in - switch option { - case values[0]: - onFieldInteraction( - .fieldChanges( - area: areaIndex, - sectionIndex: sectionIndex, - groupIndex: groupIndex, - fieldIndex: fieldIndex, - fieldChanges: .boolean(values[0]) - ) - ) - - case values[1]: - onFieldInteraction( - .fieldChanges( - area: areaIndex, - sectionIndex: sectionIndex, - groupIndex: groupIndex, - fieldIndex: fieldIndex, - fieldChanges: .boolean(values[1]) - ) - ) - - default: - return - } - } - } - - @ViewBuilder - private func fileTypeFieldView( - areaIndex: Int, - sectionIndex: Int, - groupIndex: Int, - fieldIndex: Int, - placeholder: String?, - fileName: String? - ) -> some View { - SMSFileField( - placeholder, - fileText: fileName - ) { result in - switch result { - case let .success(url): - onFieldInteraction( - .fieldChanges( - area: areaIndex, - sectionIndex: sectionIndex, - groupIndex: groupIndex, - fieldIndex: fieldIndex, - fieldChanges: .file(url.absoluteString) - ) - ) - - default: - return - } - } - } - - @ViewBuilder - private func selectTypeFieldView( - areaIndex: Int, - sectionIndex: Int, - groupIndex: Int, - fieldIndex: Int, - placeholder: String?, - selectedValue: String?, - values: [String] - ) -> some View { - SMSTextField( - placeholder ?? "", - text: Binding( - get: { selectedValue ?? "" }, - set: { _ in } - ), - isOnClear: false - ) - .disabled(true) - .overlay(alignment: .trailing) { - SMSIcon(.downChevron) - .padding(.trailing, 12) } - .simultaneousGesture( - TapGesture() - .onEnded { - optionPickerPresenter.presentOptionPicker( - options: values, - onOptionSelect: { selectedOption in - onFieldInteraction( - .fieldChanges( - area: areaIndex, - sectionIndex: sectionIndex, - groupIndex: groupIndex, - fieldIndex: fieldIndex, - fieldChanges: .select(selectedOption) - ) - ) - } - ) - } - ) } } From f43be8faf683b512c1bf1145d03d199552e8320a Mon Sep 17 00:00:00 2001 From: Sunghun Kim <81547954+kimsh153@users.noreply.github.com> Date: Tue, 9 Jul 2024 23:23:01 +0900 Subject: [PATCH 11/13] =?UTF-8?q?:test=5Ftube:=20::=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Testing/UseCase/FileUploadUseCase.swift | 13 +++++++++++++ .../Demo/Sources/AppDelegate.swift | 4 +++- .../GSMAuthenticationFormFeature/Project.swift | 3 ++- 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 Projects/Domain/FileDomain/Testing/UseCase/FileUploadUseCase.swift diff --git a/Projects/Domain/FileDomain/Testing/UseCase/FileUploadUseCase.swift b/Projects/Domain/FileDomain/Testing/UseCase/FileUploadUseCase.swift new file mode 100644 index 00000000..59b17ba6 --- /dev/null +++ b/Projects/Domain/FileDomain/Testing/UseCase/FileUploadUseCase.swift @@ -0,0 +1,13 @@ +import FileDomainInterface +import Foundation + +public final class FileUploadUseCaseSpy: FileUploadUseCase { + var executeCallCount = 0 + public func execute(file: Data, fileName: String) async throws -> String { + executeCallCount += 1 + return "A" + } + public init(executeCallCount: Int = 0) { + self.executeCallCount = executeCallCount + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift index be551369..3e175930 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift @@ -1,6 +1,7 @@ import SwiftUI @testable import GSMAuthenticationFormFeature import AuthenticationDomainTesting +import FileDomainTesting @main struct GSMAuthenticationFormDemoApp: App { @@ -83,7 +84,8 @@ final class DemoMakable { intent: GSMAuthenticationFormIntent( model: GSMAuthenticationFormModel(), fetchAuthenticationFormUseCase: FetchAuthenticationFormUseCaseSpy(), - inputAuthenticationUseCase: InputAuthenticationUseCaseSpy() + inputAuthenticationUseCase: InputAuthenticationUseCaseSpy(), + fileUploadUseCase: FileUploadUseCaseSpy() ), uiModel: uiModel ) { _ in diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Project.swift b/Projects/Feature/GSMAuthenticationFormFeature/Project.swift index 5418184b..bb6dbcfc 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Project.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Project.swift @@ -12,6 +12,7 @@ let project = Project.makeModule( .Domain.FileDomainInterface ], demoDependencies: [ - .Domain.AuthenticationDomainTesting + .Domain.AuthenticationDomainTesting, + .Domain.FileDomainTesting ] ) From 4a733aa3e7d74ef74bcf82f3217576e1a7f8241f Mon Sep 17 00:00:00 2001 From: Sunghun Kim <81547954+kimsh153@users.noreply.github.com> Date: Tue, 9 Jul 2024 23:35:53 +0900 Subject: [PATCH 12/13] =?UTF-8?q?:recycle:=20::=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=A0=81=EC=9A=A9=20=EC=95=88=20?= =?UTF-8?q?=EB=90=9C=EA=B1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/File/SMSFileField.swift | 4 ++++ .../Components/FormBuilderAreaView.swift | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Projects/Core/DesignSystem/Sources/File/SMSFileField.swift b/Projects/Core/DesignSystem/Sources/File/SMSFileField.swift index 9132ed12..f02b6442 100644 --- a/Projects/Core/DesignSystem/Sources/File/SMSFileField.swift +++ b/Projects/Core/DesignSystem/Sources/File/SMSFileField.swift @@ -39,6 +39,10 @@ public struct SMSFileField: View { isOnClear: false ) .disabled(true) + .overlay(alignment: .trailing) { + SMSIcon(.folder) + .padding(.trailing, 12) + } .simultaneousGesture( TapGesture() .onEnded { diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/FormBuilderAreaView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/FormBuilderAreaView.swift index df356b83..74dbae4a 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/FormBuilderAreaView.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/FormBuilderAreaView.swift @@ -290,15 +290,18 @@ struct FormBuilderAreaView: View { ) { result in switch result { case let .success(url): - onFieldInteraction( - .fieldChanges( - area: areaIndex, - sectionIndex: sectionIndex, - groupIndex: groupIndex, - fieldIndex: fieldIndex, - fieldChanges: .file(url.absoluteString) + if let fileData = try? Data(contentsOf: url) { + let fileName = url.lastPathComponent + onFieldInteraction( + .fieldChanges( + area: areaIndex, + sectionIndex: sectionIndex, + groupIndex: groupIndex, + fieldIndex: fieldIndex, + fieldChanges: .file(fileData, fileName) + ) ) - ) + } default: return From 5a0033ed82a4ee1134d00d0b997023ebbee76ce7 Mon Sep 17 00:00:00 2001 From: Sunghun Kim <81547954+kimsh153@users.noreply.github.com> Date: Thu, 11 Jul 2024 22:20:36 +0900 Subject: [PATCH 13/13] =?UTF-8?q?:sparkles:=20::=20API=EC=97=B0=EB=8F=99,?= =?UTF-8?q?=20ui=20=EC=A0=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DI/AuthenticationDomainBuildable.swift | 1 + .../RemoteAuthenticationDataSource.swift | 2 + .../Entity/AuthenticationStateEntity.swift | 13 ++ .../Interface/Enum/MarkingBoardEnum.swift | 6 + .../Repository/AuthenticationRepository.swift | 1 + .../FetchAuthenticationStateUseCase.swift | 5 + .../DI/AuthenticationDomainComponent.swift | 3 + .../AuthenticationStateResponseDTO.swift | 27 +++ .../RemoteAuthenticationDataSourceImpl.swift | 4 + .../Endpoint/AuthenticationEndpoint.swift | 13 ++ .../AuthenticationRepositoryImpl.swift | 4 + .../FetchAuthenticationStateUseCaseImpl.swift | 13 ++ .../FetchAuthenticationStateUseCaseSpy.swift | 15 ++ .../Demo/Sources/AppDelegate.swift | 2 +- .../DI/GSMAuthenticationComponent.swift | 2 +- .../Intent/GSMAuthenticationFormIntent.swift | 33 +++- .../GSMAuthenticationFormIntentProtocol.swift | 3 +- .../Model/GSMAuthenticationFormModel.swift | 18 ++ .../GSMAuthenticationFormModelProtocol.swift | 11 ++ .../Model/GSMAuthenticationStateModel.swift | 8 + .../Scene/GSMAuthenticationFormView.swift | 165 +++++++++++++----- 21 files changed, 298 insertions(+), 51 deletions(-) create mode 100644 Projects/Domain/AuthenticationDomain/Interface/Entity/AuthenticationStateEntity.swift create mode 100644 Projects/Domain/AuthenticationDomain/Interface/Enum/MarkingBoardEnum.swift create mode 100644 Projects/Domain/AuthenticationDomain/Interface/UseCase/FetchAuthenticationStateUseCase.swift create mode 100644 Projects/Domain/AuthenticationDomain/Sources/DTO/Response/AuthenticationStateResponseDTO.swift create mode 100644 Projects/Domain/AuthenticationDomain/Sources/UseCase/FetchAuthenticationStateUseCaseImpl.swift create mode 100644 Projects/Domain/AuthenticationDomain/Testing/FetchAuthenticationStateUseCaseSpy.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationStateModel.swift diff --git a/Projects/Domain/AuthenticationDomain/Interface/DI/AuthenticationDomainBuildable.swift b/Projects/Domain/AuthenticationDomain/Interface/DI/AuthenticationDomainBuildable.swift index 289ef321..fe5e5a25 100644 --- a/Projects/Domain/AuthenticationDomain/Interface/DI/AuthenticationDomainBuildable.swift +++ b/Projects/Domain/AuthenticationDomain/Interface/DI/AuthenticationDomainBuildable.swift @@ -1,4 +1,5 @@ public protocol AuthenticationDomainBuildable { var fetchAuthenticationFormUseCase: any FetchAuthenticationFormUseCase { get } var inputAuthenticationUseCase: any InputAuthenticationUseCase { get } + var fetchAuthenticationStateUseCase: any FetchAuthenticationStateUseCase { get } } diff --git a/Projects/Domain/AuthenticationDomain/Interface/DataSource/RemoteAuthenticationDataSource.swift b/Projects/Domain/AuthenticationDomain/Interface/DataSource/RemoteAuthenticationDataSource.swift index 3c62b8d1..0e6c292e 100644 --- a/Projects/Domain/AuthenticationDomain/Interface/DataSource/RemoteAuthenticationDataSource.swift +++ b/Projects/Domain/AuthenticationDomain/Interface/DataSource/RemoteAuthenticationDataSource.swift @@ -4,4 +4,6 @@ public protocol RemoteAuthenticationDataSource { func fetchAuthenticationForm() async throws -> AuthenticationFormEntity func inputAuthentication(req: InputAuthenticationRequestDTO) async throws + + func fetchAuthenticationState() async throws -> AuthenticationStateEntity } diff --git a/Projects/Domain/AuthenticationDomain/Interface/Entity/AuthenticationStateEntity.swift b/Projects/Domain/AuthenticationDomain/Interface/Entity/AuthenticationStateEntity.swift new file mode 100644 index 00000000..624147b8 --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Interface/Entity/AuthenticationStateEntity.swift @@ -0,0 +1,13 @@ +public struct AuthenticationStateEntity { + public let name: String + public let score: Double + public let grader: String? + public let markingBoardType: MarkingBoardType + + public init(name: String, score: Double, grader: String?, markingBoardType: MarkingBoardType) { + self.name = name + self.score = score + self.grader = grader + self.markingBoardType = markingBoardType + } +} diff --git a/Projects/Domain/AuthenticationDomain/Interface/Enum/MarkingBoardEnum.swift b/Projects/Domain/AuthenticationDomain/Interface/Enum/MarkingBoardEnum.swift new file mode 100644 index 00000000..fe91336a --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Interface/Enum/MarkingBoardEnum.swift @@ -0,0 +1,6 @@ +public enum MarkingBoardType: String, Codable { + case notSubmitted = "NOT_SUBMITTED" + case pendingReview = "PENDING_REVIEW" + case underReview = "UNDER_REVIEW" + case completed = "COMPLETED" +} diff --git a/Projects/Domain/AuthenticationDomain/Interface/Repository/AuthenticationRepository.swift b/Projects/Domain/AuthenticationDomain/Interface/Repository/AuthenticationRepository.swift index 93ca19f6..e720be1e 100644 --- a/Projects/Domain/AuthenticationDomain/Interface/Repository/AuthenticationRepository.swift +++ b/Projects/Domain/AuthenticationDomain/Interface/Repository/AuthenticationRepository.swift @@ -3,4 +3,5 @@ import Foundation public protocol AuthenticationRepository { func fetchAuthenticationForm() async throws -> AuthenticationFormEntity func inputAuthentication(req: InputAuthenticationRequestDTO) async throws + func fetchAuthenticationState() async throws -> AuthenticationStateEntity } diff --git a/Projects/Domain/AuthenticationDomain/Interface/UseCase/FetchAuthenticationStateUseCase.swift b/Projects/Domain/AuthenticationDomain/Interface/UseCase/FetchAuthenticationStateUseCase.swift new file mode 100644 index 00000000..dec52872 --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Interface/UseCase/FetchAuthenticationStateUseCase.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol FetchAuthenticationStateUseCase { + func execute() async throws -> AuthenticationStateEntity +} diff --git a/Projects/Domain/AuthenticationDomain/Sources/DI/AuthenticationDomainComponent.swift b/Projects/Domain/AuthenticationDomain/Sources/DI/AuthenticationDomainComponent.swift index 5de122ee..802d6367 100644 --- a/Projects/Domain/AuthenticationDomain/Sources/DI/AuthenticationDomainComponent.swift +++ b/Projects/Domain/AuthenticationDomain/Sources/DI/AuthenticationDomainComponent.swift @@ -14,6 +14,9 @@ public final class AuthenticationDomainComponent: Component AuthenticationStateEntity { + AuthenticationStateEntity( + name: name, + score: score, + grader: grader, + markingBoardType: markingBoardType + ) + } +} diff --git a/Projects/Domain/AuthenticationDomain/Sources/DataSource/RemoteAuthenticationDataSourceImpl.swift b/Projects/Domain/AuthenticationDomain/Sources/DataSource/RemoteAuthenticationDataSourceImpl.swift index 47bed4fd..1518f954 100644 --- a/Projects/Domain/AuthenticationDomain/Sources/DataSource/RemoteAuthenticationDataSourceImpl.swift +++ b/Projects/Domain/AuthenticationDomain/Sources/DataSource/RemoteAuthenticationDataSourceImpl.swift @@ -9,4 +9,8 @@ final class RemoteAuthenticationDataSourceImpl: BaseRemoteDataSource AuthenticationStateEntity { + try await request(.fetchAuthenticationState, dto: AuthenticationStateResponseDTO.self).toDomain() + } } diff --git a/Projects/Domain/AuthenticationDomain/Sources/Endpoint/AuthenticationEndpoint.swift b/Projects/Domain/AuthenticationDomain/Sources/Endpoint/AuthenticationEndpoint.swift index 8519c27e..3ad9385e 100644 --- a/Projects/Domain/AuthenticationDomain/Sources/Endpoint/AuthenticationEndpoint.swift +++ b/Projects/Domain/AuthenticationDomain/Sources/Endpoint/AuthenticationEndpoint.swift @@ -5,6 +5,7 @@ import Emdpoint enum AuthenticationEndpoint { case fetchAuthenticationForm case inputAuthentication(req: InputAuthenticationRequestDTO) + case fetchAuthenticationState } extension AuthenticationEndpoint: SMSEndpoint { @@ -20,6 +21,8 @@ extension AuthenticationEndpoint: SMSEndpoint { return .get("/form") case .inputAuthentication: return .post("/submit") + case .fetchAuthenticationState: + return .get("/verify") } } @@ -31,6 +34,9 @@ extension AuthenticationEndpoint: SMSEndpoint { case let .inputAuthentication(req): return .requestJSONEncodable(req) + case .fetchAuthenticationState: + return .requestPlain + default: return .requestPlain } @@ -44,6 +50,9 @@ extension AuthenticationEndpoint: SMSEndpoint { case .inputAuthentication: return .accessToken + case .fetchAuthenticationState: + return .accessToken + default: return .none } @@ -59,6 +68,10 @@ extension AuthenticationEndpoint: SMSEndpoint { return [ 500: .internalServerError ] + case .fetchAuthenticationState: + return [ + 500: .internalServerError + ] } } } diff --git a/Projects/Domain/AuthenticationDomain/Sources/Repository/AuthenticationRepositoryImpl.swift b/Projects/Domain/AuthenticationDomain/Sources/Repository/AuthenticationRepositoryImpl.swift index d32410cc..49ea1e79 100644 --- a/Projects/Domain/AuthenticationDomain/Sources/Repository/AuthenticationRepositoryImpl.swift +++ b/Projects/Domain/AuthenticationDomain/Sources/Repository/AuthenticationRepositoryImpl.swift @@ -14,4 +14,8 @@ struct AuthenticationRepositoryImpl: AuthenticationRepository { func inputAuthentication(req: InputAuthenticationRequestDTO) async throws { try await remoteAuthenticationDataSource.inputAuthentication(req: req) } + + func fetchAuthenticationState() async throws -> AuthenticationStateEntity { + try await remoteAuthenticationDataSource.fetchAuthenticationState() + } } diff --git a/Projects/Domain/AuthenticationDomain/Sources/UseCase/FetchAuthenticationStateUseCaseImpl.swift b/Projects/Domain/AuthenticationDomain/Sources/UseCase/FetchAuthenticationStateUseCaseImpl.swift new file mode 100644 index 00000000..baf0a7a2 --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Sources/UseCase/FetchAuthenticationStateUseCaseImpl.swift @@ -0,0 +1,13 @@ +import AuthenticationDomainInterface + +struct FetchAuthenticationStateUseCaseImpl: FetchAuthenticationStateUseCase { + private let authenticationRepository: any AuthenticationRepository + + init(authenticationRepository: any AuthenticationRepository) { + self.authenticationRepository = authenticationRepository + } + + func execute() async throws -> AuthenticationStateEntity { + try await authenticationRepository.fetchAuthenticationState() + } +} diff --git a/Projects/Domain/AuthenticationDomain/Testing/FetchAuthenticationStateUseCaseSpy.swift b/Projects/Domain/AuthenticationDomain/Testing/FetchAuthenticationStateUseCaseSpy.swift new file mode 100644 index 00000000..cbc3641d --- /dev/null +++ b/Projects/Domain/AuthenticationDomain/Testing/FetchAuthenticationStateUseCaseSpy.swift @@ -0,0 +1,15 @@ +import AuthenticationDomainInterface + +public final class FetchAuthenticationStateUseCaseSpy: FetchAuthenticationStateUseCase { + public var callCount = 0 + public var handler: (() async throws -> AuthenticationStateEntity)? = { + return AuthenticationStateEntity(name: "ASDFsa", score: 0, grader: nil, markingBoardType: .notSubmitted) + } + + public init() {} + + public func execute() async throws -> AuthenticationStateEntity { + guard let handler else { fatalError() } + return try await handler() + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift index 3e175930..42ca07e7 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift @@ -85,7 +85,7 @@ final class DemoMakable { model: GSMAuthenticationFormModel(), fetchAuthenticationFormUseCase: FetchAuthenticationFormUseCaseSpy(), inputAuthenticationUseCase: InputAuthenticationUseCaseSpy(), - fileUploadUseCase: FileUploadUseCaseSpy() + fileUploadUseCase: FileUploadUseCaseSpy(), fetchAuthenticationStateUseCase: FetchAuthenticationStateUseCaseSpy() ), uiModel: uiModel ) { _ in diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/DI/GSMAuthenticationComponent.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/DI/GSMAuthenticationComponent.swift index fa417ff3..7c81fee6 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/DI/GSMAuthenticationComponent.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/DI/GSMAuthenticationComponent.swift @@ -18,7 +18,7 @@ public final class GSMAuthenticationComponent: Component some View { + HStack { + Spacer() + SMSText("\(state.stateModel.name)님의 인증제는 현재 채점 중입니다", font: .title2) + .foregroundStyle(Color.sms(.neutral(.n40))) + Spacer() + } + .padding(.vertical, 32) + .background(Color.sms(.system(.white))) + .cornerRadius(16) + .shadow(color: .sms(.system(.black)).opacity(0.08), radius: 16, y: 2) + .padding(.horizontal, 20) + } + + @ViewBuilder + func underReview() -> some View { + HStack { + Spacer() + SMSText("\(state.stateModel.name)님의 인증제는 현재 채점 전입니다", font: .title2) + .foregroundStyle(Color.sms(.neutral(.n40))) + Spacer() + } + .padding(.vertical, 32) + .background(Color.sms(.system(.white))) + .cornerRadius(16) + .shadow(color: .sms(.system(.black)).opacity(0.08), radius: 16, y: 2) + .padding(.horizontal, 20) + } + + @ViewBuilder func completed() -> some View { + HStack { + Spacer() + VStack(spacing: 8) { + SMSText("\(state.stateModel.name)님의 인증제 점수는", font: .title2) + .foregroundStyle(Color.sms(.system(.black))) + .multilineTextAlignment(.center) + + SMSText("\(Int(state.stateModel.score.rounded()))점 입니다.", font: .headline3) + .foregroundStyle(Color.sms(.primary(.p2))) + .multilineTextAlignment(.center) + + SMSText("채점자 : \(state.stateModel.grader ?? "선생님")", font: .body2) + .foregroundStyle(Color.sms(.neutral(.n40))) + .multilineTextAlignment(.center) + } + Spacer() + } + .padding(.vertical, 32) + .background(Color.sms(.system(.white))) + .cornerRadius(16) + .shadow(color: .sms(.system(.black)).opacity(0.08), radius: 16, y: 2) + .padding(.horizontal, 20) + } + + @ViewBuilder + func authenticationView() -> some View { + GSMAuthenticationFormBuilderView(intent: intent, uiModel: state.uiModel) { field in + switch field { + case let .fieldChanges(area, section, group, field, fieldChanges): + switch fieldChanges { + case let .text(text): + intent.updateTextField(area: area, sectionIndex: section, groupIndex: group, fieldIndex: field, text: text) + case let .number(number): + intent.updateNumberField(area: area, sectionIndex: section, groupIndex: group, fieldIndex: field, number: number) + case let .boolean(select): + intent.updateBoolField(area: area, sectionIndex: section, groupIndex: group, fieldIndex: field, select: select) + case let .file(file, fileName): + intent.updateFileField(area: area, sectionIndex: section, groupIndex: group, fieldIndex: field, file: file, fileName: fileName) + case let .select(select): + intent.updateSelectField(area: area, sectionIndex: section, groupIndex: group, fieldIndex: field, select: select) + } + case let .groupAdd(area, section, group): + intent.appendField(area: area, sectionIndex: section, groupIndex: group) + case let .groupRemove(area, section, group): + intent.deleteField(area: area, sectionIndex: section, groupIndex: group) + } + } + .padding(.bottom, safeAreaInsets.bottom + 16) + .overlay(alignment: .bottom) { + CTAButton(text: "저장") { + intent.saveButtonDidTap(state: state) + } + .frame(maxWidth: .infinity) + .padding(.horizontal, 20) + } + .onAppear { + intent.formOnAppear() + } + } }