diff --git a/Projects/App/Sources/Application/DI/AppComponent.swift b/Projects/App/Sources/Application/DI/AppComponent.swift index 93a964ce..57b8290e 100644 --- a/Projects/App/Sources/Application/DI/AppComponent.swift +++ b/Projects/App/Sources/Application/DI/AppComponent.swift @@ -85,8 +85,8 @@ public extension AppComponent { var homeComponent: HomeComponent { HomeComponent(parent: self) } - var applyComponent: ApplyComponent { - ApplyComponent(parent: self) + var studyRoomDetailComponent: StudyRoomDetailComponent { + StudyRoomDetailComponent(parent: self) } var noticeListComponent: NoticeListComponent { NoticeListComponent(parent: self) diff --git a/Projects/App/Sources/Application/DI/Auth/AppComponent+Auth.swift b/Projects/App/Sources/Application/DI/Auth/AppComponent+Auth.swift index 1bb1ccdd..3449f7fe 100644 --- a/Projects/App/Sources/Application/DI/Auth/AppComponent+Auth.swift +++ b/Projects/App/Sources/Application/DI/Auth/AppComponent+Auth.swift @@ -63,6 +63,8 @@ public extension AppComponent { } var logoutUseCase: any LogoutUseCase { - LogoutUseCaseImpl(authRepository: authRepository) + shared { + LogoutUseCaseImpl(authRepository: authRepository) + } } } diff --git a/Projects/App/Sources/Application/NeedleGenerated.swift b/Projects/App/Sources/Application/NeedleGenerated.swift index 97f6b025..384903b9 100644 --- a/Projects/App/Sources/Application/NeedleGenerated.swift +++ b/Projects/App/Sources/Application/NeedleGenerated.swift @@ -340,16 +340,30 @@ private class HomeDependency443c4e1871277bd8432aProvider: HomeDependency { private func factory67229cdf0f755562b2b1f47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { return HomeDependency443c4e1871277bd8432aProvider(appComponent: parent1(component) as! AppComponent) } -private class ApplyDependency468ec8d97f04fe0ebd10Provider: ApplyDependency { - - - init() { - +private class StudyRoomDetailDependency00589e4f8d1416a01b43Provider: StudyRoomDetailDependency { + var fetchStudyAvailableTimeUseCase: any FetchStudyAvailableTimeUseCase { + return appComponent.fetchStudyAvailableTimeUseCase + } + var fetchSeatTypesUseCase: any FetchSeatTypesUseCase { + return appComponent.fetchSeatTypesUseCase + } + var fetchDetailStudyRoomUseCase: any FetchDetailStudyRoomUseCase { + return appComponent.fetchDetailStudyRoomUseCase + } + var applyStudyRoomSeatUseCase: any ApplyStudyRoomSeatUseCase { + return appComponent.applyStudyRoomSeatUseCase + } + var cancelStudyRoomSeatUseCase: any CancelStudyRoomSeatUseCase { + return appComponent.cancelStudyRoomSeatUseCase + } + private let appComponent: AppComponent + init(appComponent: AppComponent) { + self.appComponent = appComponent } } -/// ^->AppComponent->ApplyComponent -private func factory3cbfeafbe8b73941b232e3b0c44298fc1c149afb(_ component: NeedleFoundation.Scope) -> AnyObject { - return ApplyDependency468ec8d97f04fe0ebd10Provider() +/// ^->AppComponent->StudyRoomDetailComponent +private func factorya36f40c25dcb280bae0ff47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { + return StudyRoomDetailDependency00589e4f8d1416a01b43Provider(appComponent: parent1(component) as! AppComponent) } private class StudyRoomListDependencyef56e26c25d5de596604Provider: StudyRoomListDependency { var fetchStudyRoomListUseCase: any FetchStudyRoomListUseCase { @@ -358,6 +372,9 @@ private class StudyRoomListDependencyef56e26c25d5de596604Provider: StudyRoomList var fetchStudyAvailableTimeUseCase: any FetchStudyAvailableTimeUseCase { return appComponent.fetchStudyAvailableTimeUseCase } + var studyRoomDetailComponent: StudyRoomDetailComponent { + return appComponent.studyRoomDetailComponent + } private let appComponent: AppComponent init(appComponent: AppComponent) { self.appComponent = appComponent @@ -500,7 +517,7 @@ extension AppComponent: Registration { localTable["signupTermsComponent-SignupTermsComponent"] = { self.signupTermsComponent as Any } localTable["mainTabComponent-MainTabComponent"] = { self.mainTabComponent as Any } localTable["homeComponent-HomeComponent"] = { self.homeComponent as Any } - localTable["applyComponent-ApplyComponent"] = { self.applyComponent as Any } + localTable["studyRoomDetailComponent-StudyRoomDetailComponent"] = { self.studyRoomDetailComponent as Any } localTable["noticeListComponent-NoticeListComponent"] = { self.noticeListComponent as Any } localTable["myPageComponent-MyPageComponent"] = { self.myPageComponent as Any } localTable["changeProfileComponent-ChangeProfileComponent"] = { self.changeProfileComponent as Any } @@ -659,15 +676,20 @@ extension HomeComponent: Registration { keyPathToName[\HomeDependency.fetchWhetherNewNoticeUseCase] = "fetchWhetherNewNoticeUseCase-any FetchWhetherNewNoticeUseCase" } } -extension ApplyComponent: Registration { +extension StudyRoomDetailComponent: Registration { public func registerItems() { - + keyPathToName[\StudyRoomDetailDependency.fetchStudyAvailableTimeUseCase] = "fetchStudyAvailableTimeUseCase-any FetchStudyAvailableTimeUseCase" + keyPathToName[\StudyRoomDetailDependency.fetchSeatTypesUseCase] = "fetchSeatTypesUseCase-any FetchSeatTypesUseCase" + keyPathToName[\StudyRoomDetailDependency.fetchDetailStudyRoomUseCase] = "fetchDetailStudyRoomUseCase-any FetchDetailStudyRoomUseCase" + keyPathToName[\StudyRoomDetailDependency.applyStudyRoomSeatUseCase] = "applyStudyRoomSeatUseCase-any ApplyStudyRoomSeatUseCase" + keyPathToName[\StudyRoomDetailDependency.cancelStudyRoomSeatUseCase] = "cancelStudyRoomSeatUseCase-any CancelStudyRoomSeatUseCase" } } extension StudyRoomListComponent: Registration { public func registerItems() { keyPathToName[\StudyRoomListDependency.fetchStudyRoomListUseCase] = "fetchStudyRoomListUseCase-any FetchStudyRoomListUseCase" keyPathToName[\StudyRoomListDependency.fetchStudyAvailableTimeUseCase] = "fetchStudyAvailableTimeUseCase-any FetchStudyAvailableTimeUseCase" + keyPathToName[\StudyRoomListDependency.studyRoomDetailComponent] = "studyRoomDetailComponent-StudyRoomDetailComponent" } } extension AuthenticationEmailComponent: Registration { @@ -720,7 +742,7 @@ private func registerProviderFactory(_ componentPath: String, _ factory: @escapi #if !NEEDLE_DYNAMIC -private func register1() { +@inline(never) private func register1() { registerProviderFactory("^->AppComponent", factoryEmptyDependencyProvider) registerProviderFactory("^->AppComponent->SplashComponent", factoryace9f05f51d68f4c0677f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->SchoolConfirmationQuestionsComponent", factoryd462667f0418a53210fcf47b58f8f304c97af4d5) @@ -740,7 +762,7 @@ private func register1() { registerProviderFactory("^->AppComponent->RootComponent", factory264bfc4d4cb6b0629b40f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->SigninComponent", factory2882a056d84a613debccf47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->HomeComponent", factory67229cdf0f755562b2b1f47b58f8f304c97af4d5) - registerProviderFactory("^->AppComponent->ApplyComponent", factory3cbfeafbe8b73941b232e3b0c44298fc1c149afb) + registerProviderFactory("^->AppComponent->StudyRoomDetailComponent", factorya36f40c25dcb280bae0ff47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->StudyRoomListComponent", factory7451c5364e65ee2d46bbf47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->AuthenticationEmailComponent", factory8798d0becd9d2870112af47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->ChangePasswordComponent", factoryab7c4d87dab53e0a51b9f47b58f8f304c97af4d5) diff --git a/Projects/Features/ApplyFeature/Sources/ApplyComponent.swift b/Projects/Features/ApplyFeature/Sources/ApplyComponent.swift deleted file mode 100644 index 80c8b9c4..00000000 --- a/Projects/Features/ApplyFeature/Sources/ApplyComponent.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SwiftUI -import NeedleFoundation - -public protocol ApplyDependency: Dependency {} - -public final class ApplyComponent: Component { - public func makeView() -> some View { - Text("Text") - } -} diff --git a/Projects/Features/ApplyFeature/Sources/ApplyView.swift b/Projects/Features/ApplyFeature/Sources/ApplyView.swift deleted file mode 100644 index ccad7f89..00000000 --- a/Projects/Features/ApplyFeature/Sources/ApplyView.swift +++ /dev/null @@ -1,16 +0,0 @@ -import DesignSystem -import SwiftUI - -struct ApplyView: View { - @StateObject var viewModel: ApplyViewModel - - init( - viewModel: ApplyViewModel - ) { - _viewModel = StateObject(wrappedValue: viewModel) - } - - var body: some View { - Text("Text") - } -} diff --git a/Projects/Features/ApplyFeature/Sources/ApplyViewModel.swift b/Projects/Features/ApplyFeature/Sources/ApplyViewModel.swift deleted file mode 100644 index 62530f71..00000000 --- a/Projects/Features/ApplyFeature/Sources/ApplyViewModel.swift +++ /dev/null @@ -1,5 +0,0 @@ -import BaseFeature -import Combine - -final class ApplyViewModel: BaseViewModel { -} diff --git a/Projects/Features/ApplyFeature/Sources/Component/StudyRoomListCellView.swift b/Projects/Features/ApplyFeature/Sources/Component/StudyRoomListCellView.swift new file mode 100644 index 00000000..28ec5c93 --- /dev/null +++ b/Projects/Features/ApplyFeature/Sources/Component/StudyRoomListCellView.swift @@ -0,0 +1,41 @@ +import DesignSystem +import DomainModule +import Utility +import SwiftUI + +struct StudyRoomListCellView: View { + var studyRoomEntity: StudyRoomEntity + + var body: some View { + ZStack { + Color.System.surface + .cornerRadius(6) + + VStack(alignment: .leading, spacing: 18) { + HStack(spacing: 14) { + Text("\(studyRoomEntity.floor)층") + .dmsFont(.body(.body3), color: .System.primary) + + Text(studyRoomEntity.name) + .dmsFont(.body(.body3), color: .System.backgroundTitle) + + Spacer() + + Text("\(studyRoomEntity.inUseHeadcount)/\(studyRoomEntity.totalAvailableSeat)") + .dmsFont(.body(.body3), color: .GrayScale.gray5) + } + + Text("\(gradeToString(studyRoomEntity.availableGrade)) \(studyRoomEntity.availableSex.displayString())") + .dmsFont(.body(.body3), color: .System.primary) + } + .padding(.vertical, 16) + .padding(.horizontal, 16) + } + .frame(height: 68) + .dmsShadow(style: .surface) + } + + func gradeToString(_ grade: Int) -> String { + return grade == 0 ? "전학년" : String(grade) + "학년" + } +} diff --git a/Projects/Features/ApplyFeature/Sources/StudyRoomNoticeView.swift b/Projects/Features/ApplyFeature/Sources/Component/StudyRoomNoticeView.swift similarity index 94% rename from Projects/Features/ApplyFeature/Sources/StudyRoomNoticeView.swift rename to Projects/Features/ApplyFeature/Sources/Component/StudyRoomNoticeView.swift index 63b6b9cc..2b1cde51 100644 --- a/Projects/Features/ApplyFeature/Sources/StudyRoomNoticeView.swift +++ b/Projects/Features/ApplyFeature/Sources/Component/StudyRoomNoticeView.swift @@ -18,7 +18,7 @@ struct StudyRoomNoticeView: View { Spacer() } .background { - Color.GrayScale.gray1 + Color.System.surface } .cornerRadius(100) .padding(.horizontal, 24) diff --git a/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/Component/Seat/DMSSeatButton.swift b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/Component/Seat/DMSSeatButton.swift new file mode 100644 index 00000000..216194b6 --- /dev/null +++ b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/Component/Seat/DMSSeatButton.swift @@ -0,0 +1,31 @@ +import SwiftUI +import DataMappingModule + +public struct DMSSeatButton: View { + var text: String + var style: DMSSeatButtonStyle.Style + var color: Color + var isSelected: Bool + var action: () -> Void + + public init( + text: String = "", + style: DMSSeatButtonStyle.Style = .empty, + color: Color = .blue, + isSelected: Bool = false, + action: @escaping () -> Void = {} + ) { + self.text = text + self.style = style + self.color = color + self.isSelected = isSelected + self.action = action + } + + public var body: some View { + Button(action: action) { + Text(text) + } + .buttonStyle(DMSSeatButtonStyle(style: style, color: color, isSelected: isSelected)) + } +} diff --git a/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/Component/Seat/DMSSeatButtonStyle.swift b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/Component/Seat/DMSSeatButtonStyle.swift new file mode 100644 index 00000000..a6580333 --- /dev/null +++ b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/Component/Seat/DMSSeatButtonStyle.swift @@ -0,0 +1,110 @@ +import SwiftUI +import DataMappingModule + +public struct DMSSeatButtonStyle: ButtonStyle { + public enum Style { + case available + case unavailable + case inUse + case empty + } + var style: Style + var color: Color + var isSelected: Bool + + public func makeBody(configuration: Configuration) -> some View { + switch style { + case .available: + return AnyView(AvailableButton(configuration: configuration, color: color, isSelected: isSelected)) + case .unavailable: + return AnyView(UnAvailableButton(configuration: configuration, color: color)) + case .inUse: + return AnyView(InUseButton(configuration: configuration, color: color)) + case .empty: + return AnyView(EmptyButton()) + } + } +} + +// MARK: - Available +extension DMSSeatButtonStyle { + struct AvailableButton: View { + let configuration: ButtonStyle.Configuration + let color: Color + let isSelected: Bool + + var body: some View { + ZStack { + if isSelected { + Circle() + .strokeBorder(color) + } else { + Circle() + .foregroundColor(color) + } + + configuration.label + .dmsFont(.etc(.overline), color: isSelected ? .GrayScale.gray7 : .GrayScale.gray1) + } + .frame(width: 40, height: 40) + .dmsShadow(style: .surface) + } + } +} + +// MARK: - UnAvailable +extension DMSSeatButtonStyle { + struct UnAvailableButton: View { + let configuration: ButtonStyle.Configuration + let color: Color + + var body: some View { + ZStack { + Circle() + .foregroundColor(.GrayScale.gray4) + + Text("불가") + .dmsFont(.etc(.overline), color: .GrayScale.gray1) + } + .frame(width: 40, height: 40) + .dmsShadow(style: .surface) + + } + } +} + +// MARK: - InUse +extension DMSSeatButtonStyle { + struct InUseButton: View { + let configuration: ButtonStyle.Configuration + let color: Color + + var body: some View { + ZStack { + Circle() + .foregroundColor(color) + .opacity(0.4) + + configuration.label + .dmsFont(.etc(.overline), color: .GrayScale.gray1) + } + .frame(width: 40, height: 40) + .dmsShadow(style: .surface) + + } + } +} + +// MARK: - Empty +extension DMSSeatButtonStyle { + struct EmptyButton: View { + var body: some View { + ZStack { + Circle() + .foregroundColor(.clear) + } + .frame(width: 40, height: 40) + + } + } +} diff --git a/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/Component/StudyRoomSeatDetailView.swift b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/Component/StudyRoomSeatDetailView.swift new file mode 100644 index 00000000..0aeda1d1 --- /dev/null +++ b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/Component/StudyRoomSeatDetailView.swift @@ -0,0 +1,59 @@ +import DesignSystem +import DomainModule +import DataMappingModule +import SwiftUI +import Utility + +extension StudyRoomDetailView { + + @ViewBuilder + func studyRoomSeatDetailView() -> some View { + ScrollView([.horizontal, .vertical]) { + LazyHGrid(rows: rows) { + ForEach(viewModel.studyRoomDetail?.seats ?? [], id: \.self) { singleSeat in + DMSSeatButton( + text: generateSeatButtonText(seat: singleSeat), + style: seatStatusToSeatStyle(status: singleSeat.status), + color: Color(hex: singleSeat.type?.color ?? ""), + isSelected: singleSeat.id == viewModel.selectedSeat?.id ?? "" + ) { + if viewModel.selectedSeat?.id == singleSeat.id || singleSeat.status != .available { + withAnimation { + viewModel.selectedSeat = nil + } + } else { + withAnimation { + viewModel.selectedSeat = singleSeat + } + } + } + } + } + } + } + + func generateSeatButtonText(seat: SeatEntity) -> String { + var text: String { + seat.status == .available ? + String(seat.number ?? 0) : + seat.student?.name ?? "" + } + return text + } + + func seatStatusToSeatStyle(status: SeatStatusType) -> DMSSeatButtonStyle.Style { + switch status { + case .available: + return .available + + case .unavailable: + return .unavailable + + case .inUse: + return .inUse + + case .empty: + return .empty + } + } +} diff --git a/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/Component/StudyRoomSeatTypeView.swift b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/Component/StudyRoomSeatTypeView.swift new file mode 100644 index 00000000..d329a52e --- /dev/null +++ b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/Component/StudyRoomSeatTypeView.swift @@ -0,0 +1,27 @@ +import DesignSystem +import DomainModule +import SwiftUI +import Utility + +extension StudyRoomDetailView { + + @ViewBuilder + func studyRoomSeatTypeView() -> some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(viewModel.seatTypes, id: \.self) { seatType in + HStack { + Circle() + .foregroundColor(Color(hex: seatType.color)) + .frame(width: 10, height: 10) + Spacer() + .frame(width: 4) + + Text(seatType.name) + .dmsFont(.etc(.overline), color: .System.backgroundTitle) + } + } + } + } + } +} diff --git a/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/Component/StudyRoomSeatView.swift b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/Component/StudyRoomSeatView.swift new file mode 100644 index 00000000..a9575107 --- /dev/null +++ b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/Component/StudyRoomSeatView.swift @@ -0,0 +1,59 @@ +import DesignSystem +import DomainModule +import SwiftUI +import Utility + +extension StudyRoomDetailView { + + @ViewBuilder + func studyRoomSeatView() -> some View { + VStack { + Spacer() + .frame(height: 30) + + studyRoomSeatTypeView() + + Spacer() + .frame(height: 15) + + HStack { + Text(viewModel.studyRoomDetail?.westDescription ?? "동") + .dmsFont(.etc(.button), color: Color.PrimaryVariant.lighten1) + .rotationEffect(.degrees(-90)) + .padding(.leading, 8) + + VStack(alignment: .center) { + Text(viewModel.studyRoomDetail?.northDescription ?? "북") + .dmsFont(.etc(.button), color: Color.PrimaryVariant.lighten1) + .padding(.top, 8) + + studyRoomSeatDetailView() + + Text(viewModel.studyRoomDetail?.southDescription ?? "ska") + .dmsFont(.etc(.button), color: Color.PrimaryVariant.lighten1) + .padding(.bottom, 8) + } + + Text(viewModel.studyRoomDetail?.eastDescription ?? "서") + .dmsFont(.etc(.button), color: Color.PrimaryVariant.lighten1) + .rotationEffect(.degrees(90)) + .padding(.trailing, 8) + + } + .background { + Color.System.surface + .cornerRadius(20) + } + .overlay { + RoundedRectangle(cornerRadius: 20) + .stroke( + Color.PrimaryVariant.primary, + lineWidth: 1 + ) + } + .dmsShadow(style: .surface) + + } + } + +} diff --git a/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/StudyRoomDetailComponent.swift b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/StudyRoomDetailComponent.swift new file mode 100644 index 00000000..6282caef --- /dev/null +++ b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/StudyRoomDetailComponent.swift @@ -0,0 +1,27 @@ +import SwiftUI +import DomainModule +import NeedleFoundation + +public protocol StudyRoomDetailDependency: Dependency { + var fetchStudyAvailableTimeUseCase: any FetchStudyAvailableTimeUseCase { get } + var fetchSeatTypesUseCase: any FetchSeatTypesUseCase { get } + var fetchDetailStudyRoomUseCase: any FetchDetailStudyRoomUseCase { get } + var applyStudyRoomSeatUseCase: any ApplyStudyRoomSeatUseCase { get } + var cancelStudyRoomSeatUseCase: any CancelStudyRoomSeatUseCase { get } + +} + +public final class StudyRoomDetailComponent: Component { + public func makeView(studyRoomEntity: StudyRoomEntity) -> some View { + StudyRoomDetailView( + viewModel: .init( + studyRoomEntity: studyRoomEntity, + fetchStudyAvailableTimeUseCase: dependency.fetchStudyAvailableTimeUseCase, + fetchSeatTypesUseCase: dependency.fetchSeatTypesUseCase, + fetchDetailStudyRoomUseCase: dependency.fetchDetailStudyRoomUseCase, + applyStudyRoomSeatUseCase: dependency.applyStudyRoomSeatUseCase, + cancelStudyRoomSeatUseCase: dependency.cancelStudyRoomSeatUseCase + ) + ) + } +} diff --git a/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/StudyRoomDetailView.swift b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/StudyRoomDetailView.swift new file mode 100644 index 00000000..ed401628 --- /dev/null +++ b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/StudyRoomDetailView.swift @@ -0,0 +1,77 @@ +import BaseFeature +import DesignSystem +import DomainModule +import SwiftUI + +struct StudyRoomDetailView: View { + @StateObject var viewModel: StudyRoomDetailViewModel + @Environment(\.dismiss) var dismiss + @Environment(\.tabbarHidden) var tabbarHidden + + init( + viewModel: StudyRoomDetailViewModel + ) { + _viewModel = StateObject(wrappedValue: viewModel) + } + + var rows: [GridItem] { + Array( + repeating: GridItem(.flexible(), spacing: 40), + count: viewModel.studyRoomDetail?.totalHeightSize ?? 0 + ) + } + + var body: some View { + VStack { + StudyRoomNoticeView(text: viewModel.availableTimeString) + .padding(.top, 12) + + StudyRoomListCellView( + studyRoomEntity: viewModel.studyRoomEntity + ) + .padding(.horizontal, 24) + .padding(.top, 30) + + studyRoomDetailBackView() + .padding(.top, 20) + .padding(.bottom, 0) + .edgesIgnoringSafeArea(.bottom) + } + .dmsBackButton(dismiss: dismiss) + .navigationTitle("자습실 자리 신청") + .navigationBarTitleDisplayMode(.inline) + .dmsBackground() + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) + .onAppear { + viewModel.onAppear() + } + } + + @ViewBuilder + func studyRoomDetailBackView() -> some View { + VStack { + studyRoomSeatView() + .padding(.bottom, 6) + + HStack(spacing: 10) { + DMSWideButton(text: "취소",style: .contained, color: .GrayScale.gray4) { + viewModel.cancelStudyRoomSeat() + } + + DMSWideButton(text: "신청", style: .contained, color: .System.primary) { + viewModel.applyStudyRoomSeat() + } + .disabled(viewModel.selectedSeat == nil) + } + .padding(.bottom, 40) + + } + .padding(.horizontal, 24) + .background { + Color.System.background + .ignoresSafeArea() + .cornerRadius(20) + } + .dmsShadow(style: .surface) + } +} diff --git a/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/StudyRoomDetailViewModel.swift b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/StudyRoomDetailViewModel.swift new file mode 100644 index 00000000..de71fcc9 --- /dev/null +++ b/Projects/Features/ApplyFeature/Sources/StudyRoomDetail/StudyRoomDetailViewModel.swift @@ -0,0 +1,139 @@ +import BaseFeature +import DomainModule +import DataMappingModule +import Combine +import Foundation + +final class StudyRoomDetailViewModel: BaseViewModel { + @Published var studyRoomDetail: DetailStudyRoomEntity? + @Published var seatTypes: [SeatTypeEntity] = [] + @Published var seat: [[SeatEntity]] = [[]] + @Published var availableTimeString: String = "" + @Published var isShowingToast = false + @Published var toastMessage = "" + @Published var selectedSeat: SeatEntity? + + let studyRoomEntity: StudyRoomEntity + + private let fetchStudyAvailableTimeUseCase: any FetchStudyAvailableTimeUseCase + private let fetchSeatTypesUseCase: any FetchSeatTypesUseCase + private let fetchDetailStudyRoomUseCase: any FetchDetailStudyRoomUseCase + private let applyStudyRoomSeatUseCase: any ApplyStudyRoomSeatUseCase + private let cancelStudyRoomSeatUseCase: any CancelStudyRoomSeatUseCase + + public init( + studyRoomEntity: StudyRoomEntity, + fetchStudyAvailableTimeUseCase: any FetchStudyAvailableTimeUseCase, + fetchSeatTypesUseCase: any FetchSeatTypesUseCase, + fetchDetailStudyRoomUseCase: any FetchDetailStudyRoomUseCase, + applyStudyRoomSeatUseCase: any ApplyStudyRoomSeatUseCase, + cancelStudyRoomSeatUseCase: any CancelStudyRoomSeatUseCase + ) { + self.studyRoomEntity = studyRoomEntity + self.fetchStudyAvailableTimeUseCase = fetchStudyAvailableTimeUseCase + self.fetchSeatTypesUseCase = fetchSeatTypesUseCase + self.fetchDetailStudyRoomUseCase = fetchDetailStudyRoomUseCase + self.applyStudyRoomSeatUseCase = applyStudyRoomSeatUseCase + self.cancelStudyRoomSeatUseCase = cancelStudyRoomSeatUseCase + } + + func onAppear() { + fetchStudyAvailableTime() + fetchSeatType() + fetchDetailStudyRoom() + } + + func fetchSeatType() { + addCancellable( + fetchSeatTypesUseCase.execute() + ) { [weak self] seatTypes in + self?.seatTypes = seatTypes + } + } + + func fetchStudyAvailableTime() { + addCancellable( + fetchStudyAvailableTimeUseCase.execute() + ) { [weak self] availableTime in + let startTime = availableTime.startAt.toSmallDMSTimeString() + let endTime = availableTime.endAt.toSmallDMSTimeString() + self?.availableTimeString = "자습실 신청 시간은 \(startTime) ~ \(endTime) 까지 입니다." + } + } + + func fetchDetailStudyRoom() { + var newSeatArray: [SeatEntity] = [] + addCancellable( + fetchDetailStudyRoomUseCase.execute( + roomID: self.studyRoomEntity.id + ) + ) { [weak self] detailStudyRoom in + newSeatArray = self?.addEmptySeat( + width: detailStudyRoom.totalWidthSize, + height: detailStudyRoom.totalHeightSize, + beforeArray: detailStudyRoom.seats + ) ?? [] + + self?.studyRoomDetail = .init( + floor: detailStudyRoom.floor, + name: detailStudyRoom.name, + totalAvailableSeat: detailStudyRoom.totalAvailableSeat, + inUseHeadcount: detailStudyRoom.inUseHeadcount, + availableSex: detailStudyRoom.availableSex, + availableGrade: detailStudyRoom.availableGrade, + eastDescription: detailStudyRoom.eastDescription, + westDescription: detailStudyRoom.westDescription, + southDescription: detailStudyRoom.southDescription, + northDescription: detailStudyRoom.northDescription, + totalWidthSize: detailStudyRoom.totalWidthSize, + totalHeightSize: detailStudyRoom.totalHeightSize, + seats: newSeatArray + ) + } + } + + func applyStudyRoomSeat() { + guard let selectedSeat else { return } + addCancellable( + applyStudyRoomSeatUseCase.execute(seatID: selectedSeat.id) + ) { [weak self] _ in + self?.fetchDetailStudyRoom() + self?.isShowingToast = true + self?.toastMessage = "자습실 신청이 완료되었습니다." + } + } + + func cancelStudyRoomSeat() { + addCancellable( + cancelStudyRoomSeatUseCase.execute() + ) { [weak self] _ in + self?.fetchDetailStudyRoom() + self?.isShowingToast = true + self?.toastMessage = "자습실 취소가 완료되었습니다." + } + } + + func addEmptySeat(width: Int, height: Int, beforeArray: [SeatEntity]) -> [SeatEntity] { + if beforeArray.count == width * height { + return beforeArray + } + + var newArray = [SeatEntity?]( + repeating: nil, + count: width * height + ) + + beforeArray.forEach { value in + let index = (value.widthLocation - 1) * height + value.heightLocation - 1 + newArray[index] = value + } + return newArray.compactMap { + $0 ?? SeatEntity( + id: UUID().uuidString, + widthLocation: 1, + heightLocation: 1, + status: .empty + ) + } + } +} diff --git a/Projects/Features/ApplyFeature/Sources/StudyroomApplication/StudyRoomListComponent.swift b/Projects/Features/ApplyFeature/Sources/StudyroomApplication/StudyRoomListComponent.swift index cfc18565..a180eda6 100644 --- a/Projects/Features/ApplyFeature/Sources/StudyroomApplication/StudyRoomListComponent.swift +++ b/Projects/Features/ApplyFeature/Sources/StudyroomApplication/StudyRoomListComponent.swift @@ -6,6 +6,7 @@ import NeedleFoundation public protocol StudyRoomListDependency: Dependency { var fetchStudyRoomListUseCase: any FetchStudyRoomListUseCase { get } var fetchStudyAvailableTimeUseCase: any FetchStudyAvailableTimeUseCase { get } + var studyRoomDetailComponent: StudyRoomDetailComponent { get } } public final class StudyRoomListComponent: Component { @@ -14,7 +15,8 @@ public final class StudyRoomListComponent: Component { viewModel: .init( fetchStudyRoomListUseCase: dependency.fetchStudyRoomListUseCase, fetchStudyAvailableTimeUseCase: dependency.fetchStudyAvailableTimeUseCase - ) + ), + studyRoomDetailComponent: dependency.studyRoomDetailComponent ) } } diff --git a/Projects/Features/ApplyFeature/Sources/StudyroomApplication/StudyRoomListView.swift b/Projects/Features/ApplyFeature/Sources/StudyroomApplication/StudyRoomListView.swift index 24649439..08aa565e 100644 --- a/Projects/Features/ApplyFeature/Sources/StudyroomApplication/StudyRoomListView.swift +++ b/Projects/Features/ApplyFeature/Sources/StudyroomApplication/StudyRoomListView.swift @@ -1,14 +1,19 @@ import DesignSystem +import DomainModule import Utility import SwiftUI struct StudyRoomListView: View { @StateObject var viewModel: StudyRoomListViewModel + private let studyRoomDetailComponent: StudyRoomDetailComponent + @Environment(\.tabbarHidden) var tabbarHidden init( - viewModel: StudyRoomListViewModel + viewModel: StudyRoomListViewModel, + studyRoomDetailComponent: StudyRoomDetailComponent ) { _viewModel = StateObject(wrappedValue: viewModel) + self.studyRoomDetailComponent = studyRoomDetailComponent } var body: some View { @@ -18,71 +23,40 @@ struct StudyRoomListView: View { if viewModel.isStudyRoomTime { StudyRoomNoticeView(text: viewModel.rangeString) } - LazyVStack(spacing: 16) { Spacer() .frame(height: 10) - ForEach(viewModel.studyRoomList, id: \.self) { studyRoomList in - studyRoomListCellView( - name: studyRoomList.name, - floor: studyRoomList.floor, - inUseHeadcount: studyRoomList.inUseHeadcount, - totalAvailableSeat: studyRoomList.totalAvailableSeat, - availableGrade: studyRoomList.availableGrade, - availableSex: studyRoomList.availableSex.displayString() - ) - .padding(.top, 5) - .listRowInsets(EdgeInsets()) + Button { + viewModel.isNavigateDetail.toggle() + viewModel.studyRoomDetail = studyRoomList + } label: { + StudyRoomListCellView(studyRoomEntity: studyRoomList) + .padding(.top, 5) + .padding(.bottom, 10) + } } + .padding(.horizontal, 24) } - .padding(.horizontal, 24) } - } - .navigationTitle("자습실 신청") - .navigationBarTitleDisplayMode(.inline) - .dmsBackground() - .onAppear { - viewModel.fetchStudyRoomList() - viewModel.fetchStudyAvailableTime() - } - } - } - - @ViewBuilder - func studyRoomListCellView( - name: String, - floor: Int, - inUseHeadcount: Int, - totalAvailableSeat: Int, - availableGrade: Int, - availableSex: String - ) -> some View { - ZStack { - Color.System.surface - .cornerRadius(6) - - VStack(alignment: .leading, spacing: 18) { - HStack(spacing: 14) { - Text("\(floor)층") - .dmsFont(.body(.body3), color: .System.primary) - - Text(name) - .dmsFont(.body(.body3), color: .System.backgroundTitle) - - Spacer() - - Text("\(inUseHeadcount)/\(totalAvailableSeat)") - .dmsFont(.body(.body3), color: .GrayScale.gray5) + .navigationTitle("자습실 신청") + .navigationBarTitleDisplayMode(.inline) + .dmsBackground() + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) + .onAppear { + viewModel.fetchStudyRoomList() + viewModel.fetchStudyAvailableTime() } - - Text("\(availableGrade)학년 \(availableSex)") - .dmsFont(.body(.body3), color: .System.primary) + .onChange(of: viewModel.isNavigateDetail) { newValue in + withAnimation { + tabbarHidden.wrappedValue = newValue + } + } + .navigate( + to: studyRoomDetailComponent.makeView(studyRoomEntity: viewModel.studyRoomDetail), + when: $viewModel.isNavigateDetail + ) } - .padding(.vertical, 16) - .padding(.horizontal, 16) } - .frame(height: 68) - .dmsShadow(style: .surface) } } diff --git a/Projects/Features/ApplyFeature/Sources/StudyroomApplication/StudyRoomListViewModel.swift b/Projects/Features/ApplyFeature/Sources/StudyroomApplication/StudyRoomListViewModel.swift index ece69755..f31e5654 100644 --- a/Projects/Features/ApplyFeature/Sources/StudyroomApplication/StudyRoomListViewModel.swift +++ b/Projects/Features/ApplyFeature/Sources/StudyroomApplication/StudyRoomListViewModel.swift @@ -11,6 +11,27 @@ final class StudyRoomListViewModel: BaseViewModel { @Published var isShowingToast = false @Published var toastMessage = "" @Published var studyAvailableTime: StudyAvailableTimeEntity? + @Published var isNavigateDetail: Bool = false + @Published var studyRoomDetail: StudyRoomEntity = .init( + id: "", + floor: 0, + name: "", + availableGrade: 0, + availableSex: .all, + inUseHeadcount: 0, + totalAvailableSeat: 0, + isMine: false + ) + + var rangeString: String { + if let time = studyAvailableTime { + let text = "자습실 신청 시간은 " + time.startAt.toHourAndMinuteDSMDateString() + + " ~ " + time.endAt.toHourAndMinuteDSMDateString() + " 까지 입니다." + return text + } else { + return "" + } + } private let fetchStudyRoomListUseCase: any FetchStudyRoomListUseCase private let fetchStudyAvailableTimeUseCase: any FetchStudyAvailableTimeUseCase @@ -29,8 +50,6 @@ final class StudyRoomListViewModel: BaseViewModel { fetchStudyRoomListUseCase.execute() ) { [weak self] studyRoomList in self?.studyRoomList = studyRoomList - } onReceiveError: { error in - print(error) } } @@ -39,18 +58,6 @@ final class StudyRoomListViewModel: BaseViewModel { fetchStudyAvailableTimeUseCase.execute() ) { [weak self] studyAvailableTime in self?.studyAvailableTime = studyAvailableTime - } onReceiveError: { error in - print(error) - } - } - - var rangeString: String { - if let time = studyAvailableTime { - let text = "자습실 신청 시간은 " + time.startAt.toHourAndMinuteDSMDateString() + - " ~ " + time.endAt.toHourAndMinuteDSMDateString() + " 까지 입니다." - return text - } else { - return "" } } } diff --git a/Projects/Features/BaseFeature/Sources/DMSSelectionTab.swift b/Projects/Features/BaseFeature/Sources/DMSSelectionTab.swift new file mode 100644 index 00000000..1441299e --- /dev/null +++ b/Projects/Features/BaseFeature/Sources/DMSSelectionTab.swift @@ -0,0 +1,12 @@ +import SwiftUI + +struct DMSSelectionTabbKey: EnvironmentKey { + static var defaultValue: Binding = .constant(.home) +} + +public extension EnvironmentValues { + var dmsSelectionTabbKey: Binding { + get { self[DMSSelectionTabbKey.self] } + set { self[DMSSelectionTabbKey.self] = newValue } + } +} diff --git a/Projects/Features/BaseFeature/Sources/TabFlow.swift b/Projects/Features/BaseFeature/Sources/TabFlow.swift new file mode 100644 index 00000000..4e858a7b --- /dev/null +++ b/Projects/Features/BaseFeature/Sources/TabFlow.swift @@ -0,0 +1,8 @@ +import Foundation + +public enum TabFlow: Int { + case home + case apply + case notice + case myPage +} diff --git a/Projects/Features/FindIDFeature/Sources/Component/SchoolSelectButtonView.swift b/Projects/Features/FindIDFeature/Sources/Component/SchoolSelectButtonView.swift index 2f1d5b37..cd3bf9a1 100644 --- a/Projects/Features/FindIDFeature/Sources/Component/SchoolSelectButtonView.swift +++ b/Projects/Features/FindIDFeature/Sources/Component/SchoolSelectButtonView.swift @@ -21,34 +21,35 @@ struct SchoolSelectButtonView: View { } public var body: some View { - VStack(alignment: .leading, spacing: 10) { - HStack(alignment: .bottom) { - Text(placeholderText) - .dmsFont(.body(.body1), color: textColor) + Menu { + ForEach(schoolList) { school in + Button("\(school.name)", action: { + placeholderText = school.name + onCommit(school) + }) + } + .labelsHidden() + .pickerStyle(InlinePickerStyle()) + } label: { + VStack(alignment: .leading, spacing: 10) { + HStack(alignment: .bottom) { + Text(placeholderText) + .dmsFont(.body(.body1), color: textColor) - Spacer() + Spacer() - Menu { - ForEach(schoolList) { school in - Button("\(school.name)", action: { - placeholderText = school.name - onCommit(school) - }) - } - .labelsHidden() - .pickerStyle(InlinePickerStyle()) - } label: { Image(systemName: "chevron.down") .foregroundColor(.GrayScale.gray5) .frame(width: 24, height: 24) } - } - .overlay(alignment: .bottom) { - Rectangle() - .foregroundColor(.GrayScale.gray5) - .frame(height: 1) - .offset(y: 7) + .overlay(alignment: .bottom) { + Rectangle() + .foregroundColor(.GrayScale.gray5) + .frame(height: 1) + .offset(y: 7) + } } } + } } diff --git a/Projects/Features/FindIDFeature/Sources/FindIDView.swift b/Projects/Features/FindIDFeature/Sources/FindIDView.swift index 3793bdf2..64d0250d 100644 --- a/Projects/Features/FindIDFeature/Sources/FindIDView.swift +++ b/Projects/Features/FindIDFeature/Sources/FindIDView.swift @@ -62,6 +62,7 @@ struct FindIDView: View { .disabled(!viewModel.isFindEnabled) .padding(.bottom, 40) } + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) .dmsToast(isShowing: $viewModel.isShowingToast, message: viewModel.toastMessage, style: .success) .padding(.horizontal, 24) .dmsBackground() diff --git a/Projects/Features/HomeFeature/Sources/Component/MealCarouselView.swift b/Projects/Features/HomeFeature/Sources/Component/MealCarouselView.swift index cec40597..2f07e318 100644 --- a/Projects/Features/HomeFeature/Sources/Component/MealCarouselView.swift +++ b/Projects/Features/HomeFeature/Sources/Component/MealCarouselView.swift @@ -80,7 +80,7 @@ struct MealCarouselView: View { UIScreen.main.bounds.height * 0.4103 ) .background { - Color.GrayScale.gray1 + Color.System.surface .cornerRadius(20) } .overlay { diff --git a/Projects/Features/HomeFeature/Sources/Component/NoticeView.swift b/Projects/Features/HomeFeature/Sources/Component/NoticeView.swift index 852cfc20..6a28affa 100644 --- a/Projects/Features/HomeFeature/Sources/Component/NoticeView.swift +++ b/Projects/Features/HomeFeature/Sources/Component/NoticeView.swift @@ -26,7 +26,7 @@ struct NoticeView: View { .padding(.trailing, 8) } .background { - Color.GrayScale.gray1 + Color.System.surface } .cornerRadius(100) .padding(.horizontal, 24) diff --git a/Projects/Features/HomeFeature/Sources/HomeView.swift b/Projects/Features/HomeFeature/Sources/HomeView.swift index d93e73c7..094599ee 100644 --- a/Projects/Features/HomeFeature/Sources/HomeView.swift +++ b/Projects/Features/HomeFeature/Sources/HomeView.swift @@ -1,3 +1,4 @@ +import BaseFeature import DesignSystem import SwiftUI import Utility @@ -6,6 +7,8 @@ struct HomeView: View { @StateObject var viewModel: HomeViewModel @State var isShowingCalendar = false @Environment(\.tabbarHidden) var tabbarHidden + @Environment(\.dmsSelectionTabbKey) var dmsSelectionTabbKey + @EnvironmentObject var appState: AppState init(viewModel: HomeViewModel) { _viewModel = StateObject(wrappedValue: viewModel) @@ -36,7 +39,11 @@ struct HomeView: View { ScrollView(showsIndicators: false) { if viewModel.isExistNewNotice { - NoticeView() + Button { + dmsSelectionTabbKey.wrappedValue = .notice + } label: { + NoticeView() + } } Text("오늘의 급식") @@ -55,7 +62,7 @@ struct HomeView: View { } } } - .onChange(of: viewModel.selectedDate) { newValue in + .onChange(of: viewModel.selectedDate) { _ in viewModel.onChangeSelectedDate() } .onAppear { @@ -68,8 +75,9 @@ struct HomeView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Text("로고") - .dmsFont(.title(.title1), color: .GrayScale.gray7) + DMSImage(.logoHorizontal, renderingMode: .original) + .scaledToFit() + .frame(height: 28) } } .dmsBackground() diff --git a/Projects/Features/MainTabFeature/Sources/MainTabView.swift b/Projects/Features/MainTabFeature/Sources/MainTabView.swift index b315d65c..2f5b8736 100644 --- a/Projects/Features/MainTabFeature/Sources/MainTabView.swift +++ b/Projects/Features/MainTabFeature/Sources/MainTabView.swift @@ -7,15 +7,23 @@ import BaseFeature import DesignSystem import Utility -enum TabFlow: Int { - case home - case apply - case notice - case myPage -} +// swiftlint: disable large_tuple struct MainTabView: View { @State var selection: TabFlow = .home @State var tabbarHidden = false + @EnvironmentObject var appState: AppState + + var tabItem: [(String, String, TabFlow)] { + var tabItems: [(String, String, TabFlow)] = [ + ("house", "홈", .home), + ("megaphone", "안내", .notice), + ("person", "마이페이지", .myPage) + ] + if appState.features.studyRoomService { + tabItems.insert(("plus.bubble", "신청", .apply), at: 1) + } + return tabItems + } private let homeComponent: HomeComponent private let studyRoomListComponent: StudyRoomListComponent @@ -40,8 +48,10 @@ struct MainTabView: View { homeComponent.makeView() .tag(TabFlow.home) - studyRoomListComponent.makeView() - .tag(TabFlow.apply) + if appState.features.studyRoomService { + studyRoomListComponent.makeView() + .tag(TabFlow.apply) + } noticeComponent.makeView() .tag(TabFlow.notice) @@ -61,10 +71,12 @@ struct MainTabView: View { .cornerRadius(24, corners: [.topLeft, .topRight]) .ignoresSafeArea() } - .dmsShadow(style: .tabbar) + .dmsShadow(style: .tabbar) + .environment(\.dmsSelectionTabbKey, $selection) } } } + .environment(\.dmsSelectionTabbKey, $selection) .onAppear { UITabBar.hideTabBar() } @@ -72,13 +84,6 @@ struct MainTabView: View { @ViewBuilder func tabbarView() -> some View { - let tabItem: [(String, String, TabFlow)] = [ - ("house", "홈", .home), - ("plus.bubble", "신청", .apply), - ("megaphone", "안내", .notice), - ("person", "마이페이지", .myPage) - ] - HStack { Spacer() diff --git a/Projects/Features/MyPageFeature/Sources/ChangeProfile/ChangeProfileView.swift b/Projects/Features/MyPageFeature/Sources/ChangeProfile/ChangeProfileView.swift index 817ad2c7..414561a8 100644 --- a/Projects/Features/MyPageFeature/Sources/ChangeProfile/ChangeProfileView.swift +++ b/Projects/Features/MyPageFeature/Sources/ChangeProfile/ChangeProfileView.swift @@ -63,6 +63,7 @@ struct ChangeProfileView: View { } .imagePicker(isShow: $isShowingImagePicker, uiImage: $viewModel.selectedImage) .cameraPicker(isShow: $isShowingCameraPicker, uiImage: $viewModel.selectedImage) + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) .dmsBackButton(dismiss: dismiss) .padding(.horizontal, 24) .dmsBackground() diff --git a/Projects/Features/MyPageFeature/Sources/CheckPassword/CheckPasswordView.swift b/Projects/Features/MyPageFeature/Sources/CheckPassword/CheckPasswordView.swift index ecad5502..66a6f20e 100644 --- a/Projects/Features/MyPageFeature/Sources/CheckPassword/CheckPasswordView.swift +++ b/Projects/Features/MyPageFeature/Sources/CheckPassword/CheckPasswordView.swift @@ -7,6 +7,7 @@ struct CheckPasswordView: View { @StateObject var viewModel: CheckPasswordViewModel let modifyPasswordComponent: ModifyPasswordComponent @Environment(\.dismiss) var dismiss + @Environment(\.rootPresentationMode) var rootPresentationMode init( viewModel: CheckPasswordViewModel, @@ -45,11 +46,12 @@ struct CheckPasswordView: View { .padding(.horizontal, 24) .dmsBackground() .dmsToast(isShowing: $viewModel.isShowingToast, message: viewModel.toastMessage, style: .success) + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) .ignoresSafeArea(.keyboard, edges: .bottom) .navigate( to: modifyPasswordComponent.makeView( currentPassword: viewModel.password - ), + ).environment(\.rootPresentationMode, rootPresentationMode), when: $viewModel.isSuccessCheckPassword ) } diff --git a/Projects/Features/MyPageFeature/Sources/CheckPassword/CheckPasswordViewModel.swift b/Projects/Features/MyPageFeature/Sources/CheckPassword/CheckPasswordViewModel.swift index ce79f6d7..be0f0c27 100644 --- a/Projects/Features/MyPageFeature/Sources/CheckPassword/CheckPasswordViewModel.swift +++ b/Projects/Features/MyPageFeature/Sources/CheckPassword/CheckPasswordViewModel.swift @@ -1,6 +1,7 @@ import BaseFeature import Combine import DomainModule +import Utility final class CheckPasswordViewModel: BaseViewModel { @Published var password = "" { diff --git a/Projects/Features/MyPageFeature/Sources/ModifyPassword/ModifyPasswordView.swift b/Projects/Features/MyPageFeature/Sources/ModifyPassword/ModifyPasswordView.swift index d9765c00..c31e53ba 100644 --- a/Projects/Features/MyPageFeature/Sources/ModifyPassword/ModifyPasswordView.swift +++ b/Projects/Features/MyPageFeature/Sources/ModifyPassword/ModifyPasswordView.swift @@ -11,6 +11,7 @@ struct ModifyPasswordView: View { @FocusState private var focusField: FocusField? @StateObject var viewModel: ModifyPasswordViewModel @Environment(\.dismiss) var dismiss + @Environment(\.rootPresentationMode) var rootPresentationMode init( viewModel: ModifyPasswordViewModel @@ -69,10 +70,12 @@ struct ModifyPasswordView: View { .padding(.horizontal, 24) .dmsBackground() .dmsToast(isShowing: $viewModel.isShowingToast, message: viewModel.toastMessage, style: .success) + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) .ignoresSafeArea(.keyboard, edges: .bottom) .onChange(of: viewModel.isSuccessRenewalPassword) { newValue in if newValue { - NavigationUtil.popToRootView() + print("A") + rootPresentationMode.wrappedValue.dismiss() } } } diff --git a/Projects/Features/MyPageFeature/Sources/ModifyPassword/ModifyPasswordViewModel.swift b/Projects/Features/MyPageFeature/Sources/ModifyPassword/ModifyPasswordViewModel.swift index 32a64b56..a63faa89 100644 --- a/Projects/Features/MyPageFeature/Sources/ModifyPassword/ModifyPasswordViewModel.swift +++ b/Projects/Features/MyPageFeature/Sources/ModifyPassword/ModifyPasswordViewModel.swift @@ -1,6 +1,7 @@ import BaseFeature import Combine import DomainModule +import Utility final class ModifyPasswordViewModel: BaseViewModel { @Published var password = "" { diff --git a/Projects/Features/MyPageFeature/Sources/MyPage/MyPageComponent.swift b/Projects/Features/MyPageFeature/Sources/MyPage/MyPageComponent.swift index 959d78fd..6283aca0 100644 --- a/Projects/Features/MyPageFeature/Sources/MyPage/MyPageComponent.swift +++ b/Projects/Features/MyPageFeature/Sources/MyPage/MyPageComponent.swift @@ -23,5 +23,6 @@ public final class MyPageComponent: Component { checkPasswordComponent: self.dependency.checkPasswordComponent ) } + .navigationViewStyle(.stack) } } diff --git a/Projects/Features/MyPageFeature/Sources/MyPage/MyPageView.swift b/Projects/Features/MyPageFeature/Sources/MyPage/MyPageView.swift index 0a650181..35b95f07 100644 --- a/Projects/Features/MyPageFeature/Sources/MyPage/MyPageView.swift +++ b/Projects/Features/MyPageFeature/Sources/MyPage/MyPageView.swift @@ -38,11 +38,12 @@ struct MyPageView: View { Spacer() ZStack(alignment: .bottomTrailing) { - AsyncImage(url: URL(string: "")) { image in + AsyncImage(url: viewModel.profile?.profileImageURL) { image in image .resizable() .frame(width: 74, height: 74) .clipShape(Circle()) + .scaledToFill() } placeholder: { Color.GrayScale.gray5 .frame(width: 74, height: 74) @@ -166,7 +167,8 @@ struct MyPageView: View { when: $viewModel.isNavigateChangeProfile ) .navigate( - to: checkPasswordComponent.makeView(), + to: checkPasswordComponent.makeView() + .environment(\.rootPresentationMode, $viewModel.isNavigateChangePassword), when: $viewModel.isNavigateChangePassword ) .navigate( @@ -185,7 +187,7 @@ struct MyPageView: View { .padding(.vertical, 15) .padding(.horizontal, 20) .background { - Color.GrayScale.gray1 + Color.System.surface .dmsShadow(style: .surface) } } diff --git a/Projects/Features/MyPageFeature/Sources/RewardPointDetail/RewardPointDetailView.swift b/Projects/Features/MyPageFeature/Sources/RewardPointDetail/RewardPointDetailView.swift index ac796a22..c5fac704 100644 --- a/Projects/Features/MyPageFeature/Sources/RewardPointDetail/RewardPointDetailView.swift +++ b/Projects/Features/MyPageFeature/Sources/RewardPointDetail/RewardPointDetailView.swift @@ -54,6 +54,7 @@ struct RewardPointDetailView: View { .navigationTitle("상벌점 현황") .navigationBarTitleDisplayMode(.inline) .dmsBackground() + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) } @ViewBuilder diff --git a/Projects/Features/NoticeFeature/Sources/Detail/NoticeDetailView.swift b/Projects/Features/NoticeFeature/Sources/Detail/NoticeDetailView.swift index de322d01..9db06721 100644 --- a/Projects/Features/NoticeFeature/Sources/Detail/NoticeDetailView.swift +++ b/Projects/Features/NoticeFeature/Sources/Detail/NoticeDetailView.swift @@ -32,10 +32,14 @@ struct NoticeDetailView: View { Spacer() } } + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) .navigationTitle("공지") .navigationBarTitleDisplayMode(.inline) .padding(.horizontal, 24) .dmsBackground() .dmsBackButton(dismiss: dismiss) + .onAppear { + viewModel.onAppear() + } } } diff --git a/Projects/Features/NoticeFeature/Sources/Detail/NoticeDetailViewModel.swift b/Projects/Features/NoticeFeature/Sources/Detail/NoticeDetailViewModel.swift index afd1e115..67fc9799 100644 --- a/Projects/Features/NoticeFeature/Sources/Detail/NoticeDetailViewModel.swift +++ b/Projects/Features/NoticeFeature/Sources/Detail/NoticeDetailViewModel.swift @@ -21,8 +21,9 @@ final class NoticeDetailViewModel: BaseViewModel { ) { self.fetchDetailNoticeUseCase = fetchDetailNoticeUseCase self.id = id - super.init() + } + func onAppear() { addCancellable( fetchDetailNoticeUseCase.execute(id: id) ) { [weak self] noticeDetail in @@ -30,5 +31,6 @@ final class NoticeDetailViewModel: BaseViewModel { self?.content = noticeDetail.content self?.date = noticeDetail.createdAt } + } } diff --git a/Projects/Features/NoticeFeature/Sources/NoticeList/NoticeListView.swift b/Projects/Features/NoticeFeature/Sources/NoticeList/NoticeListView.swift index 436f8565..8164637f 100644 --- a/Projects/Features/NoticeFeature/Sources/NoticeList/NoticeListView.swift +++ b/Projects/Features/NoticeFeature/Sources/NoticeList/NoticeListView.swift @@ -55,6 +55,10 @@ struct NoticeListView: View { .navigationTitle("공지") .navigationBarTitleDisplayMode(.inline) .dmsBackground() + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) + .onAppear { + viewModel.onAppear() + } } } diff --git a/Projects/Features/NoticeFeature/Sources/NoticeList/NoticeListViewModel.swift b/Projects/Features/NoticeFeature/Sources/NoticeList/NoticeListViewModel.swift index 1475fd9e..bc7e92b4 100644 --- a/Projects/Features/NoticeFeature/Sources/NoticeList/NoticeListViewModel.swift +++ b/Projects/Features/NoticeFeature/Sources/NoticeList/NoticeListViewModel.swift @@ -15,11 +15,9 @@ final class NoticeListViewModel: BaseViewModel { fetchNoticeListUseCase: any FetchNoticeListUseCase ) { self.fetchNoticeListUseCase = fetchNoticeListUseCase - super.init() - fetchNoticeList() } - func fetchNoticeList() { + func onAppear() { addCancellable( fetchNoticeListUseCase.execute( order: noticeOrderType @@ -31,6 +29,6 @@ final class NoticeListViewModel: BaseViewModel { func orderTypeButtonDidTap() { self.noticeOrderType = noticeOrderType == .new ? .old : .new - fetchNoticeList() + onAppear() } } diff --git a/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailView.swift b/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailView.swift index 45e74337..02dd58e5 100644 --- a/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailView.swift +++ b/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailView.swift @@ -3,8 +3,9 @@ import DesignSystem struct AuthenticationEmailView: View { @StateObject var viewModel: AuthenticationEmailViewModel - let changePasswordComponent: ChangePasswordComponent + private let changePasswordComponent: ChangePasswordComponent @Environment(\.dismiss) var dismiss + @State var isViewDidLoad = false init( viewModel: AuthenticationEmailViewModel, @@ -46,8 +47,15 @@ struct AuthenticationEmailView: View { } .padding(.horizontal, 24) .hideKeyboardWhenTap() + .onChange(of: viewModel.authCode) { newValue in + if newValue.count == 6 { + viewModel.verifyEmailAuthCode() + } + } .onAppear { - viewModel.sendEmailAuthCode() + if !isViewDidLoad { + viewModel.sendEmailAuthCode() + } } .dmsToast(isShowing: $viewModel.isShowingToast, message: viewModel.toastMessage, style: .success) .navigate( @@ -62,6 +70,6 @@ struct AuthenticationEmailView: View { when: $viewModel.isNavigateChangePassword ) .dmsBackButton(dismiss: dismiss) - + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) } } diff --git a/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailViewModel.swift b/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailViewModel.swift index 07e657bc..8c46408c 100644 --- a/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailViewModel.swift +++ b/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailViewModel.swift @@ -36,13 +36,6 @@ final class AuthenticationEmailViewModel: BaseViewModel { self.authenticationEmailParam = authenticationEmailParam super.init() - addCancellable( - $authCode.setFailureType(to: DmsError.self).eraseToAnyPublisher() - ) { [weak self] code in - if code.count >= 6 { - self?.verifyEmailAuthCode() - } - } addCancellable( timer.setFailureType(to: DmsError.self).eraseToAnyPublisher() ) { [weak self] _ in @@ -65,9 +58,11 @@ final class AuthenticationEmailViewModel: BaseViewModel { } func verifyEmailAuthCode() { + let email = authenticationEmailParam.email + .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" addCancellable( verifyAuthCodeUseCase.execute( - req: .init(email: authenticationEmailParam.email, authCode: authCode, type: .signup) + req: .init(email: email, authCode: authCode, type: .password) ) ) { [weak self] _ in self?.isNavigateChangePassword = true diff --git a/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordView.swift b/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordView.swift index a115bc4e..8251a91f 100644 --- a/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordView.swift +++ b/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordView.swift @@ -66,6 +66,7 @@ struct ChangePasswordView: View { .dmsBackButton(dismiss: dismiss) .padding(.horizontal, 24) .dmsBackground() + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) .dmsToast(isShowing: $viewModel.isShowingToast, message: viewModel.toastMessage, style: .success) .ignoresSafeArea(.keyboard, edges: .bottom) .alert("비밀번호가 변경되었습니다.", isPresented: $viewModel.isSuccessRenewalPassword) { diff --git a/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordViewModel.swift b/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordViewModel.swift index 6ca2b972..80f8c525 100644 --- a/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordViewModel.swift +++ b/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordViewModel.swift @@ -58,8 +58,6 @@ final class ChangePasswordViewModel: BaseViewModel { ) ) { [weak self] _ in self?.isSuccessRenewalPassword = true - } onReceiveError: { [weak self] _ in - self?.isShowingToast = true } } diff --git a/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationView.swift b/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationView.swift index bb812373..441827ce 100644 --- a/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationView.swift +++ b/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationView.swift @@ -10,7 +10,7 @@ struct EnterInformationView: View { @FocusState private var focusField: FocusField? @StateObject var viewModel: EnterInformationViewModel - let authenticationEmailComponent: AuthenticationEmailComponent + private let authenticationEmailComponent: AuthenticationEmailComponent @Environment(\.dismiss) var dismiss public init( @@ -45,7 +45,6 @@ struct EnterInformationView: View { if viewModel.isShowFoundEmail { BlockEmailView(email: $viewModel.blockEmail) } - } .padding(.top, 68) .transition(.slide) @@ -72,7 +71,7 @@ struct EnterInformationView: View { DMSWideButton(text: "다음", color: .PrimaryVariant.primary) { viewModel.nextButtonDidTap() } - .disabled(!viewModel.isNextButtonEnabled ) + .disabled(!viewModel.isNextButtonEnabled) .padding(.bottom, 40) } .onAppear { @@ -88,10 +87,10 @@ struct EnterInformationView: View { ), when: $viewModel.isNavigateAuthenticationEmail ) + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) .dmsBackButton(dismiss: dismiss) .frame(maxWidth: .infinity) .ignoresSafeArea(.keyboard, edges: .bottom) .padding(.horizontal, 24) - } } diff --git a/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationViewModel.swift b/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationViewModel.swift index 4b92c11d..233237a2 100644 --- a/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationViewModel.swift +++ b/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationViewModel.swift @@ -6,7 +6,7 @@ final class EnterInformationViewModel: BaseViewModel { @Published var email = "" { didSet { isErrorOcuured = false } } - @Published var blockEmail = "082****@naver.com" + @Published var blockEmail = "" @Published var id = "" { didSet { isErrorOcuured = false } } diff --git a/Projects/Features/SigninFeature/Sources/SigninView.swift b/Projects/Features/SigninFeature/Sources/SigninView.swift index fb34a7bc..a1bb9931 100644 --- a/Projects/Features/SigninFeature/Sources/SigninView.swift +++ b/Projects/Features/SigninFeature/Sources/SigninView.swift @@ -49,7 +49,12 @@ struct SigninView: View { .textContentType(.username) .focused($focusField, equals: .id) - SecureDMSFloatingTextField("비밀번호", text: $viewModel.password) { + SecureDMSFloatingTextField( + "비밀번호", + text: $viewModel.password, + isError: viewModel.isErrorOcuured, + errorMessage: viewModel.errorMessage + ) { viewModel.signinButtonDidTap() } .textContentType(.password) @@ -116,7 +121,7 @@ struct SigninView: View { } .navigationTitle("로그인") .navigationBarTitleDisplayMode(.inline) - .dmsToast(isShowing: $viewModel.isShowingToast, message: viewModel.toastMessage, style: .success) + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) .padding(.horizontal, 24) .dmsBackground() .navigate( @@ -125,6 +130,12 @@ struct SigninView: View { ) .ignoresSafeArea(.keyboard, edges: .bottom) } + .hideKeyboardWhenTap() + .onChange(of: viewModel.dmsFeatures) { newValue in + if let newValue { + appState.features = newValue + } + } .onChange(of: viewModel.isSuccessSignin) { newValue in guard newValue else { return } appState.sceneFlow = .main diff --git a/Projects/Features/SigninFeature/Sources/SigninViewModel.swift b/Projects/Features/SigninFeature/Sources/SigninViewModel.swift index d165309c..4586be6e 100644 --- a/Projects/Features/SigninFeature/Sources/SigninViewModel.swift +++ b/Projects/Features/SigninFeature/Sources/SigninViewModel.swift @@ -7,8 +7,7 @@ final class SigninViewModel: BaseViewModel { @Published var password = "" @Published var isOnAutoSignin = true @Published var isSuccessSignin = false - @Published var isShowingToast = false - @Published var toastMessage = "" + @Published var dmsFeatures: DmsFeatures? var isSigninEnabled: Bool { !id.isEmpty && !password.isEmpty @@ -22,7 +21,15 @@ final class SigninViewModel: BaseViewModel { func signinButtonDidTap() { guard isSigninEnabled else { return } - addCancellable(signinUseCase.execute(req: .init(accountID: id, password: password))) { [weak self] _ in + addCancellable( + signinUseCase.execute( + req: .init( + accountID: id, + password: password + ) + ) + ) { [weak self] feature in + self?.dmsFeatures = feature self?.isSuccessSignin = true } } diff --git a/Projects/Features/SignupFeature/Sources/SchoolCode/SchoolCodeView.swift b/Projects/Features/SignupFeature/Sources/SchoolCode/SchoolCodeView.swift index 05a06ca8..964ae943 100644 --- a/Projects/Features/SignupFeature/Sources/SchoolCode/SchoolCodeView.swift +++ b/Projects/Features/SignupFeature/Sources/SchoolCode/SchoolCodeView.swift @@ -39,6 +39,7 @@ struct SchoolCodeView: View { viewModel.schoolCode = "" } .hideKeyboardWhenTap() + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) .padding(.horizontal, 24) .ignoresSafeArea(.keyboard, edges: .bottom) .dmsBackground() diff --git a/Projects/Features/SignupFeature/Sources/SchoolConfirmationQuestions/SchoolConfirmationQuestionsView.swift b/Projects/Features/SignupFeature/Sources/SchoolConfirmationQuestions/SchoolConfirmationQuestionsView.swift index 7a07cd3e..81b391bc 100644 --- a/Projects/Features/SignupFeature/Sources/SchoolConfirmationQuestions/SchoolConfirmationQuestionsView.swift +++ b/Projects/Features/SignupFeature/Sources/SchoolConfirmationQuestions/SchoolConfirmationQuestionsView.swift @@ -63,6 +63,7 @@ struct SchoolConfirmationQuestionsView: View { .padding(.top, 24) .padding(.bottom, 40) } + .hideKeyboardWhenTap() .dmsToast(isShowing: $viewModel.isShowingToast, message: viewModel.toastMessage, style: .success) .dmsBackButton(dismiss: dismiss) .padding(.horizontal, 24) @@ -79,6 +80,7 @@ struct SchoolConfirmationQuestionsView: View { schoolAnswer: viewModel.answer ) ), - when: $viewModel.isNavigateSignupEmailVerify) + when: $viewModel.isNavigateSignupEmailVerify + ) } } diff --git a/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyView.swift b/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyView.swift index 687bbeb1..263292cb 100644 --- a/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyView.swift +++ b/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyView.swift @@ -5,6 +5,7 @@ import SwiftUI struct SignupEmailAuthCodeVerifyView: View { @StateObject var viewModel: SignupEmailAuthCodeVerifyViewModel @Environment(\.dismiss) var dismiss + @State var isViewDidLoad = false private let idSettingComponent: IDSettingComponent init( @@ -44,18 +45,23 @@ struct SignupEmailAuthCodeVerifyView: View { .padding(.top, 32) .padding(.bottom, 40) } + .onAppear { + if !isViewDidLoad { + viewModel.sendEmailAuthCode() + } + } + .onChange(of: viewModel.authCode) { newValue in + if newValue.count == 6 { + viewModel.verifyEmailAuthCode() + } + } .dmsBackButton(dismiss: dismiss) .padding(.horizontal, 24) .dmsBackground() .hideKeyboardWhenTap() - .onAppear { - viewModel.sendEmailAuthCode() - } .ignoresSafeArea(.keyboard, edges: .bottom) + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) .dmsToast(isShowing: $viewModel.isShowingToast, message: viewModel.toastMessage, style: .success) - .onChange(of: viewModel.isNavigateSignupID, perform: { newValue in - print("navigate-signup-id", newValue) - }) .navigate( to: idSettingComponent.makeView( idSettingParam: .init( diff --git a/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyViewModel.swift b/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyViewModel.swift index dda8134c..51691e88 100644 --- a/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyViewModel.swift +++ b/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyViewModel.swift @@ -39,16 +39,6 @@ final class SignupEmailAuthCodeVerifyViewModel: BaseViewModel { guard let self, self.timeRemaining > 0 else { return } self.timeRemaining -= 1 } - - addCancellable( - $authCode.setFailureType(to: DmsError.self) - .debounce(for: 0.5, scheduler: RunLoop.main) - .eraseToAnyPublisher() - ) { [weak self] code in - if code.count == 6 { - self?.verifyEmailAuthCode() - } - } } func sendEmailAuthCode() { diff --git a/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyView.swift b/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyView.swift index 588a23a0..a73b6c81 100644 --- a/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyView.swift +++ b/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyView.swift @@ -68,6 +68,7 @@ struct SignupEmailVerifyView: View { } } .dmsBackButton(dismiss: dismiss) + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) .padding(.horizontal, 24) } } diff --git a/Projects/Features/SignupFeature/Sources/SignupPassword/SignupPasswordView.swift b/Projects/Features/SignupFeature/Sources/SignupPassword/SignupPasswordView.swift index cfb1f066..7117bf5e 100644 --- a/Projects/Features/SignupFeature/Sources/SignupPassword/SignupPasswordView.swift +++ b/Projects/Features/SignupFeature/Sources/SignupPassword/SignupPasswordView.swift @@ -66,6 +66,7 @@ struct SignupPasswordView: View { .onAppear { focusField = .password } + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) .dmsBackButton(dismiss: dismiss) .navigate( to: signupProfileImageComponent.makeView( diff --git a/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageView.swift b/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageView.swift index f1cbe725..6fa9dacb 100644 --- a/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageView.swift +++ b/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageView.swift @@ -64,6 +64,7 @@ struct SignupProfileImageView: View { } .imagePicker(isShow: $isShowingImagePicker, uiImage: $viewModel.selectedImage) .cameraPicker(isShow: $isShowingCameraPicker, uiImage: $viewModel.selectedImage) + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) .dmsBackButton(dismiss: dismiss) .padding(.horizontal, 24) .dmsBackground() diff --git a/Projects/Features/SignupFeature/Sources/SignupTerms/SignupTermsView.swift b/Projects/Features/SignupFeature/Sources/SignupTerms/SignupTermsView.swift index 255b70e5..6fea5d04 100644 --- a/Projects/Features/SignupFeature/Sources/SignupTerms/SignupTermsView.swift +++ b/Projects/Features/SignupFeature/Sources/SignupTerms/SignupTermsView.swift @@ -1,9 +1,9 @@ import DesignSystem import SwiftUI +import Utility struct SignupTermsView: View { @StateObject var viewModel: SignupTermsViewModel - @Environment(\.rootPresentationMode) var rootPresentationMode @Environment(\.dismiss) var dismiss init( @@ -50,10 +50,11 @@ struct SignupTermsView: View { } .dmsBackground() .dmsBackButton(dismiss: dismiss) + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) .padding(.horizontal, 24) .alert(viewModel.alertMessage, isPresented: $viewModel.isShowingAlert) { Button("로그인 화면으로", role: .cancel) { - rootPresentationMode.wrappedValue.toggle() + NavigationUtil.popToRootView() } } } diff --git a/Projects/Modules/ErrorModule/Sources/DmsError.swift b/Projects/Modules/ErrorModule/Sources/DmsError.swift index 2f164055..bc9453ba 100644 --- a/Projects/Modules/ErrorModule/Sources/DmsError.swift +++ b/Projects/Modules/ErrorModule/Sources/DmsError.swift @@ -35,9 +35,11 @@ public enum DmsError: Error { // MARK: - Users case currentPasswordMismatch + case photoCapacityIsLarge // MARK: - StudyRooms case seatIsAlreadyExist + case notFoundAppliedSeat } extension DmsError: LocalizedError { @@ -109,9 +111,15 @@ extension DmsError: LocalizedError { case .currentPasswordMismatch: return "유효하지 않은 비밀번호입니다." + case .photoCapacityIsLarge: + return "사진의 최대 용량을 초과했습니다." + // MARK: - StudyRooms case .seatIsAlreadyExist: return "이미 신청된 자리입니다" + + case .notFoundAppliedSeat: + return "신청한 자리가 없습니다" } } } diff --git a/Projects/Modules/Utility/Sources/DateUtil.swift b/Projects/Modules/Utility/Sources/DateUtil.swift index c705678f..4739af21 100644 --- a/Projects/Modules/Utility/Sources/DateUtil.swift +++ b/Projects/Modules/Utility/Sources/DateUtil.swift @@ -17,7 +17,7 @@ public extension String { func toDMSTime() -> Date { let formatter = DateFormatter() - formatter.dateFormat = "hh:MM:dd" + formatter.dateFormat = "HH:mm:ss" formatter.locale = Locale(identifier: "ko_kr") return formatter.date(from: self) ?? .init() } @@ -31,6 +31,13 @@ public extension Date { return formatter.string(from: self) } + func toSmallDMSTimeString() -> String { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm" + formatter.locale = Locale(identifier: "ko_kr") + return formatter.string(from: self) + } + func toSmallDMSDateString() -> String { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" diff --git a/Projects/Modules/Utility/Sources/HexColor.swift b/Projects/Modules/Utility/Sources/HexColor.swift new file mode 100644 index 00000000..00308dda --- /dev/null +++ b/Projects/Modules/Utility/Sources/HexColor.swift @@ -0,0 +1,28 @@ +import SwiftUI + +public extension Color { + init(hex: String) { + let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int: UInt64 = 0 + Scanner(string: hex).scanHexInt64(&int) + let alpha, red, grenn, blue: UInt64 + switch hex.count { + case 3: // RGB (12-bit) + (alpha, red, grenn, blue) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (alpha, red, grenn, blue) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (alpha, red, grenn, blue) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (alpha, red, grenn, blue) = (1, 1, 1, 0) + } + + self.init( + .sRGB, + red: Double(red) / 255, + green: Double(grenn) / 255, + blue: Double(blue) / 255, + opacity: Double(alpha) / 255 + ) + } +} diff --git a/Projects/Services/APIKit/Sources/SchoolAPI.swift b/Projects/Services/APIKit/Sources/SchoolAPI.swift index e23d5cf9..d897e744 100644 --- a/Projects/Services/APIKit/Sources/SchoolAPI.swift +++ b/Projects/Services/APIKit/Sources/SchoolAPI.swift @@ -19,7 +19,7 @@ extension SchoolAPI: DmsAPI { public var urlPath: String { switch self { case .fetchSchoolList: - return "/" + return "" case let .fetchSchoolQuestion(authCode): return "/question/\(authCode)" diff --git a/Projects/Services/APIKit/Sources/StudentsAPI.swift b/Projects/Services/APIKit/Sources/StudentsAPI.swift index e52f4953..e35c6657 100644 --- a/Projects/Services/APIKit/Sources/StudentsAPI.swift +++ b/Projects/Services/APIKit/Sources/StudentsAPI.swift @@ -31,7 +31,7 @@ extension StudentsAPI: DmsAPI { return "/email/duplication" case .renewalPassword: - return "/password/intialization" + return "/password/initialization" case let .findID(req): return "/account-id/\(req.schoolID)" diff --git a/Projects/Services/APIKit/Sources/StudyRoomsAPI.swift b/Projects/Services/APIKit/Sources/StudyRoomsAPI.swift index f3ab57f9..e47b32b9 100644 --- a/Projects/Services/APIKit/Sources/StudyRoomsAPI.swift +++ b/Projects/Services/APIKit/Sources/StudyRoomsAPI.swift @@ -22,13 +22,13 @@ extension StudyRoomsAPI: DmsAPI { return "/available-time" case .fetchSeatTypes: - return "types" + return "/types" case .fetchStudyRoomList: return "/list/students" case let .fetchDetailStudyRoom(id): - return "\(id)/students" + return "/\(id)/students" case let .applyStudyRoomSeat(id): return "/seats/\(id)" @@ -68,6 +68,19 @@ extension StudyRoomsAPI: DmsAPI { 500: .internalServerError ] + case .fetchDetailStudyRoom: + return [ + 400: .badRequest, + 500: .photoCapacityIsLarge + ] + + case .cancelStudyRoomSeat: + return [ + 400: .badRequest, + 404: .notFoundAppliedSeat, + 500: .internalServerError + ] + default: return [ 400: .badRequest, diff --git a/Projects/Services/DataMappingModule/Sources/Base/DmsFeaturesResponseDTO.swift b/Projects/Services/DataMappingModule/Sources/Base/DmsFeaturesResponseDTO.swift index f02870c9..679522b5 100644 --- a/Projects/Services/DataMappingModule/Sources/Base/DmsFeaturesResponseDTO.swift +++ b/Projects/Services/DataMappingModule/Sources/Base/DmsFeaturesResponseDTO.swift @@ -5,13 +5,9 @@ public struct DmsFeaturesResponseDTO: Decodable { } public struct DmsFeaturesDTO: Decodable { - public let mealService: Bool - public let noticeService: Bool - public let pointService: Bool + public let studyRoomService: Bool enum CodingKeys: String, CodingKey { - case mealService = "meal_service" - case noticeService = "notice_service" - case pointService = "point_service" + case studyRoomService = "study_room_service" } } diff --git a/Projects/Services/DataModule/Sources/Auth/Repositories/Stub/AuthRepositoryStub.swift b/Projects/Services/DataModule/Sources/Auth/Repositories/Stub/AuthRepositoryStub.swift index c7a851dd..78fbb2fb 100644 --- a/Projects/Services/DataModule/Sources/Auth/Repositories/Stub/AuthRepositoryStub.swift +++ b/Projects/Services/DataModule/Sources/Auth/Repositories/Stub/AuthRepositoryStub.swift @@ -9,7 +9,7 @@ public struct AuthRepositoryStub: AuthRepository { public func logout() {} public func signin(req: SigninRequestDTO) -> AnyPublisher { - Just(DmsFeatures(mealService: false, noticeService: false, pointService: false)) + Just(DmsFeatures(studyRoomService: false)) .setFailureType(to: DmsError.self) .eraseToAnyPublisher() } @@ -25,7 +25,7 @@ public struct AuthRepositoryStub: AuthRepository { } public func reissueToken() -> AnyPublisher { - Just(DmsFeatures(mealService: false, noticeService: false, pointService: false)) + Just(DmsFeatures(studyRoomService: false)) .setFailureType(to: DmsError.self) .eraseToAnyPublisher() } diff --git a/Projects/Services/DataModule/Sources/Auth/UseCases/Fake/SigninUseCaseFake.swift b/Projects/Services/DataModule/Sources/Auth/UseCases/Fake/SigninUseCaseFake.swift index 1fdd1bd9..5c9197b8 100644 --- a/Projects/Services/DataModule/Sources/Auth/UseCases/Fake/SigninUseCaseFake.swift +++ b/Projects/Services/DataModule/Sources/Auth/UseCases/Fake/SigninUseCaseFake.swift @@ -9,7 +9,7 @@ public struct SigninUseCaseFake: SigninUseCase { public func execute(req: SigninRequestDTO) -> AnyPublisher { if req.accountID == "baekteun" && req.password == "baekteun" { - return Just(DmsFeatures(mealService: false, noticeService: false, pointService: false)) + return Just(DmsFeatures(studyRoomService: false)) .setFailureType(to: DmsError.self) .delay(for: 1, scheduler: DispatchQueue.main) .eraseToAnyPublisher() diff --git a/Projects/Services/DomainModule/Sources/Entities/DmsFeatures.swift b/Projects/Services/DomainModule/Sources/Entities/DmsFeatures.swift index 3a354850..f0c9f1f6 100644 --- a/Projects/Services/DomainModule/Sources/Entities/DmsFeatures.swift +++ b/Projects/Services/DomainModule/Sources/Entities/DmsFeatures.swift @@ -2,22 +2,14 @@ import Foundation public struct DmsFeatures: Equatable { public init( - mealService: Bool, - noticeService: Bool, - pointService: Bool + studyRoomService: Bool ) { - self.mealService = mealService - self.noticeService = noticeService - self.pointService = pointService + self.studyRoomService = studyRoomService } public init() { - self.mealService = false - self.noticeService = false - self.pointService = false + self.studyRoomService = false } - public let mealService: Bool - public let noticeService: Bool - public let pointService: Bool + public let studyRoomService: Bool } diff --git a/Projects/Services/DomainModule/Sources/Entities/SeatEntity.swift b/Projects/Services/DomainModule/Sources/Entities/SeatEntity.swift index a746da80..515809ca 100644 --- a/Projects/Services/DomainModule/Sources/Entities/SeatEntity.swift +++ b/Projects/Services/DomainModule/Sources/Entities/SeatEntity.swift @@ -1,7 +1,7 @@ import Foundation import DataMappingModule -public struct SeatEntity: Equatable { +public struct SeatEntity: Equatable, Hashable { public init( id: String, widthLocation: Int, diff --git a/Projects/Services/DomainModule/Sources/Entities/SeatStudentEntity.swift b/Projects/Services/DomainModule/Sources/Entities/SeatStudentEntity.swift index 8b08c186..6541f781 100644 --- a/Projects/Services/DomainModule/Sources/Entities/SeatStudentEntity.swift +++ b/Projects/Services/DomainModule/Sources/Entities/SeatStudentEntity.swift @@ -1,6 +1,6 @@ import Foundation -public struct SeatStudentEntity: Equatable { +public struct SeatStudentEntity: Equatable, Hashable { public init(id: String, name: String) { self.id = id self.name = name diff --git a/Projects/Services/DomainModule/Sources/Entities/SeatTypeEntity.swift b/Projects/Services/DomainModule/Sources/Entities/SeatTypeEntity.swift index 02058630..bcc6e068 100644 --- a/Projects/Services/DomainModule/Sources/Entities/SeatTypeEntity.swift +++ b/Projects/Services/DomainModule/Sources/Entities/SeatTypeEntity.swift @@ -1,6 +1,6 @@ import Foundation -public struct SeatTypeEntity: Equatable, Decodable { +public struct SeatTypeEntity: Equatable, Decodable, Hashable { public init(id: String, name: String, color: String) { self.id = id self.name = name diff --git a/Projects/Services/NetworkModule/Sources/Auth/Remote/Stub/RemoteAuthDataSourceStub.swift b/Projects/Services/NetworkModule/Sources/Auth/Remote/Stub/RemoteAuthDataSourceStub.swift index bdca3f12..3f8fef41 100644 --- a/Projects/Services/NetworkModule/Sources/Auth/Remote/Stub/RemoteAuthDataSourceStub.swift +++ b/Projects/Services/NetworkModule/Sources/Auth/Remote/Stub/RemoteAuthDataSourceStub.swift @@ -7,7 +7,7 @@ public struct RemoteAuthDataSourceStub: RemoteAuthDataSource { public init() {} public func signin(req: SigninRequestDTO) -> AnyPublisher { - Just(DmsFeatures(mealService: false, noticeService: false, pointService: false)) + Just(DmsFeatures(studyRoomService: false)) .setFailureType(to: DmsError.self) .eraseToAnyPublisher() } @@ -23,7 +23,7 @@ public struct RemoteAuthDataSourceStub: RemoteAuthDataSource { } public func reissueToken() -> AnyPublisher { - Just(DmsFeatures(mealService: false, noticeService: false, pointService: false)) + Just(DmsFeatures(studyRoomService: false)) .setFailureType(to: DmsError.self) .eraseToAnyPublisher() } diff --git a/Projects/Services/NetworkModule/Sources/Base/DataTransfer/DmsFeaturesDataTransfer.swift b/Projects/Services/NetworkModule/Sources/Base/DataTransfer/DmsFeaturesDataTransfer.swift index 49a76481..7f4af614 100644 --- a/Projects/Services/NetworkModule/Sources/Base/DataTransfer/DmsFeaturesDataTransfer.swift +++ b/Projects/Services/NetworkModule/Sources/Base/DataTransfer/DmsFeaturesDataTransfer.swift @@ -4,9 +4,7 @@ import DomainModule public extension DmsFeaturesResponseDTO { func toDomain() -> DmsFeatures { DmsFeatures( - mealService: features.mealService, - noticeService: features.noticeService, - pointService: features.pointService + studyRoomService: features.studyRoomService ) } } diff --git a/Projects/Services/NetworkModule/Sources/StudyRooms/Remote/Impl/DataTransfer/FetchDetailStudyRoomDataTransfer.swift b/Projects/Services/NetworkModule/Sources/StudyRooms/Remote/Impl/DataTransfer/FetchDetailStudyRoomDataTransfer.swift index df9047ee..b802d82f 100644 --- a/Projects/Services/NetworkModule/Sources/StudyRooms/Remote/Impl/DataTransfer/FetchDetailStudyRoomDataTransfer.swift +++ b/Projects/Services/NetworkModule/Sources/StudyRooms/Remote/Impl/DataTransfer/FetchDetailStudyRoomDataTransfer.swift @@ -14,7 +14,7 @@ public extension FetchDetailStudyRoomResponseDTO { westDescription: westDescription, southDescription: southDescription, northDescription: northDescription, - totalWidthSize: totalAvailableSeat, + totalWidthSize: totalWidthSize, totalHeightSize: totalHeightSize, seats: seats.map { $0.toDomain() } ) diff --git a/Projects/UsertInterfaces/DesignSystem/Resources/Icons.xcassets/DMSHorizontal.imageset/Contents.json b/Projects/UsertInterfaces/DesignSystem/Resources/Icons.xcassets/DMSHorizontal.imageset/Contents.json index b63ec851..67a22184 100644 --- a/Projects/UsertInterfaces/DesignSystem/Resources/Icons.xcassets/DMSHorizontal.imageset/Contents.json +++ b/Projects/UsertInterfaces/DesignSystem/Resources/Icons.xcassets/DMSHorizontal.imageset/Contents.json @@ -5,15 +5,48 @@ "idiom" : "universal", "scale" : "1x" }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "LOGO - HORIZONTAL.png", + "idiom" : "universal", + "scale" : "1x" + }, { "filename" : "dms-horizontal 1.svg", "idiom" : "universal", "scale" : "2x" }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "LOGO - HORIZONTAL@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, { "filename" : "dms-horizontal 2.svg", "idiom" : "universal", "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "LOGO - HORIZONTAL@3x.png", + "idiom" : "universal", + "scale" : "3x" } ], "info" : { diff --git a/Projects/UsertInterfaces/DesignSystem/Resources/Icons.xcassets/DMSHorizontal.imageset/LOGO - HORIZONTAL.png b/Projects/UsertInterfaces/DesignSystem/Resources/Icons.xcassets/DMSHorizontal.imageset/LOGO - HORIZONTAL.png new file mode 100644 index 00000000..4c287f08 Binary files /dev/null and b/Projects/UsertInterfaces/DesignSystem/Resources/Icons.xcassets/DMSHorizontal.imageset/LOGO - HORIZONTAL.png differ diff --git a/Projects/UsertInterfaces/DesignSystem/Resources/Icons.xcassets/DMSHorizontal.imageset/LOGO - HORIZONTAL@2x.png b/Projects/UsertInterfaces/DesignSystem/Resources/Icons.xcassets/DMSHorizontal.imageset/LOGO - HORIZONTAL@2x.png new file mode 100644 index 00000000..69d1a845 Binary files /dev/null and b/Projects/UsertInterfaces/DesignSystem/Resources/Icons.xcassets/DMSHorizontal.imageset/LOGO - HORIZONTAL@2x.png differ diff --git a/Projects/UsertInterfaces/DesignSystem/Resources/Icons.xcassets/DMSHorizontal.imageset/LOGO - HORIZONTAL@3x.png b/Projects/UsertInterfaces/DesignSystem/Resources/Icons.xcassets/DMSHorizontal.imageset/LOGO - HORIZONTAL@3x.png new file mode 100644 index 00000000..e935f5d9 Binary files /dev/null and b/Projects/UsertInterfaces/DesignSystem/Resources/Icons.xcassets/DMSHorizontal.imageset/LOGO - HORIZONTAL@3x.png differ