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