From c95290282d523c4e82ca2994cedf2b61b1d8aaed Mon Sep 17 00:00:00 2001 From: llghdud921 Date: Wed, 25 Oct 2023 00:53:48 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20apple=20sign=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- attendance-ios.xcodeproj/project.pbxproj | 8 + .../Source/Extensions/Foundation/Date.swift | 20 ++ attendance-ios/Source/Model/Status.swift | 2 +- .../Source/New/Domain/MemberInfoUseCase.swift | 8 + .../Source/New/Feature/App/AppCore.swift | 18 +- .../AttendanceCode/AttendanceCodeCore.swift | 154 +++++++++++++++ .../AttendanceCode/AttendanceCodeView.swift | 187 ++++++++++++++++++ .../Feature/OnBoarding/OnboardingCore.swift | 25 ++- .../Feature/OnBoarding/OnboardingView.swift | 171 ++++++++-------- .../Feature/ScoreCheck/ScoreChartCore.swift | 5 +- .../Feature/ScoreCheck/ScoreCheckCore.swift | 12 +- .../New/Feature/Setting/SettingCore.swift | 23 ++- .../New/Feature/Setting/SettingView.swift | 12 +- .../New/Feature/SignUp/SignUpCodeCore.swift | 21 +- .../Source/New/Feature/Tab/HomeTabCore.swift | 127 ++++++++++++ .../Source/New/Feature/Tab/HomeTabView.swift | 154 ++++++++++----- .../Feature/TeamSelect/TeamSelectView.swift | 6 +- .../TodaySession/TodaySessionCore.swift | 18 +- .../TodaySession/TodaySessionView.swift | 12 +- 19 files changed, 819 insertions(+), 164 deletions(-) create mode 100644 attendance-ios/Source/New/Feature/AttendanceCode/AttendanceCodeCore.swift create mode 100644 attendance-ios/Source/New/Feature/AttendanceCode/AttendanceCodeView.swift diff --git a/attendance-ios.xcodeproj/project.pbxproj b/attendance-ios.xcodeproj/project.pbxproj index 59a8170..26749e3 100644 --- a/attendance-ios.xcodeproj/project.pbxproj +++ b/attendance-ios.xcodeproj/project.pbxproj @@ -121,6 +121,8 @@ F373D2372A59382A00EBA96B /* YPText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D2362A59382A00EBA96B /* YPText.swift */; }; F373D2402A593E3C00EBA96B /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D23F2A593E3C00EBA96B /* Color.swift */; }; F373D2422A5942F000EBA96B /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D2412A5942F000EBA96B /* Font.swift */; }; + F37AB8AE2AE7BFCE00B2C9C7 /* AttendanceCodeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37AB8AD2AE7BFCE00B2C9C7 /* AttendanceCodeCore.swift */; }; + F37AB8B02AE7C2D700B2C9C7 /* AttendanceCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37AB8AF2AE7C2D700B2C9C7 /* AttendanceCodeView.swift */; }; F37AD34C2A5F066C006694D1 /* NavigationBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37AD34B2A5F066C006694D1 /* NavigationBarView.swift */; }; F37AD34E2A6302F4006694D1 /* SignUpPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37AD34D2A6302F4006694D1 /* SignUpPositionView.swift */; }; F37AD3502A630311006694D1 /* SignUpPositionCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37AD34F2A630311006694D1 /* SignUpPositionCore.swift */; }; @@ -287,6 +289,8 @@ F373D2362A59382A00EBA96B /* YPText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YPText.swift; sourceTree = ""; }; F373D23F2A593E3C00EBA96B /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; F373D2412A5942F000EBA96B /* Font.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Font.swift; sourceTree = ""; }; + F37AB8AD2AE7BFCE00B2C9C7 /* AttendanceCodeCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttendanceCodeCore.swift; sourceTree = ""; }; + F37AB8AF2AE7C2D700B2C9C7 /* AttendanceCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttendanceCodeView.swift; sourceTree = ""; }; F37AD34B2A5F066C006694D1 /* NavigationBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarView.swift; sourceTree = ""; }; F37AD34D2A6302F4006694D1 /* SignUpPositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpPositionView.swift; sourceTree = ""; }; F37AD34F2A630311006694D1 /* SignUpPositionCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpPositionCore.swift; sourceTree = ""; }; @@ -926,6 +930,8 @@ F3C9AB812AE5F88A00A589C9 /* AttendanceCode */ = { isa = PBXGroup; children = ( + F37AB8AD2AE7BFCE00B2C9C7 /* AttendanceCodeCore.swift */, + F37AB8AF2AE7C2D700B2C9C7 /* AttendanceCodeView.swift */, ); path = AttendanceCode; sourceTree = ""; @@ -1179,6 +1185,7 @@ 52EFD5F6280FFA3200065FEB /* HomeTotalScoreTableViewCell.swift in Sources */, 5E6B7FFD283271C100F4D88F /* Attendances.swift in Sources */, 5EF69ED927AAACBA007E9735 /* PolicyViewController.swift in Sources */, + F37AB8AE2AE7BFCE00B2C9C7 /* AttendanceCodeCore.swift in Sources */, 5EF06A6B2822B4B10021D25B /* Array.swift in Sources */, F315F04B2A6315BE0068B96B /* SignUpCodeCore.swift in Sources */, 52A85C57281A4A3000593749 /* BaseNavigationBarView.swift in Sources */, @@ -1207,6 +1214,7 @@ 5EED02C027D7854F00C52637 /* AlertView.swift in Sources */, F3C9AB802AE5F30D00A589C9 /* TodaySessionCoordinatorView.swift in Sources */, F3B159ED2A59547100BD0BFC /* OnboardingCore.swift in Sources */, + F37AB8B02AE7C2D700B2C9C7 /* AttendanceCodeView.swift in Sources */, F363C9782A641AB20024E0A2 /* KeychainManager.swift in Sources */, F363C97C2A641B0B0024E0A2 /* KeychainAccount.swift in Sources */, F363C9712A6415570024E0A2 /* MemberInfoDependency.swift in Sources */, diff --git a/attendance-ios/Source/Extensions/Foundation/Date.swift b/attendance-ios/Source/Extensions/Foundation/Date.swift index 391b388..7a71777 100644 --- a/attendance-ios/Source/Extensions/Foundation/Date.swift +++ b/attendance-ios/Source/Extensions/Foundation/Date.swift @@ -53,6 +53,16 @@ extension Date { guard let other = other else { return false } return isPast(than: other) } + + func isPastBeforeFiveMinuate(than other: Date?) -> Bool { + let calendar = Calendar.current + guard let other = other, + let otherDate = calendar.date(byAdding: .minute, value: -5, to: other) else { + return false + } + + return isPast(than: otherDate) + } /// 현재 날짜가 다른 날짜보다 미래인지를 반환합니다. func isFuture(than other: Date) -> Bool { @@ -65,6 +75,16 @@ extension Date { guard let other = other else { return false } return isFuture(than: other) } + + func isFutureBeforeFiveMinuate(than other: Date?) -> Bool { + let calendar = Calendar.current + guard let other = other, + let otherDate = calendar.date(byAdding: .minute, value: -5, to: other) else { + return false + } + + return self.compare(otherDate) == .orderedAscending + } /// 현재 날짜가 다른 날짜와 같은 날짜인지 반환합니다. func isPresent(than other: Date) -> Bool { diff --git a/attendance-ios/Source/Model/Status.swift b/attendance-ios/Source/Model/Status.swift index 9ce24e9..c65780e 100644 --- a/attendance-ios/Source/Model/Status.swift +++ b/attendance-ios/Source/Model/Status.swift @@ -8,7 +8,7 @@ import UIKit import SwiftUI -enum Status: Codable, CaseIterable { +enum Status: Codable, CaseIterable, Equatable { case normal case late case absent diff --git a/attendance-ios/Source/New/Domain/MemberInfoUseCase.swift b/attendance-ios/Source/New/Domain/MemberInfoUseCase.swift index 2b25426..0a5a42b 100644 --- a/attendance-ios/Source/New/Domain/MemberInfoUseCase.swift +++ b/attendance-ios/Source/New/Domain/MemberInfoUseCase.swift @@ -49,7 +49,15 @@ final class MemberInfoUseCase { } } + func updateMemberAttendances(memberId: Int, attendances: [Attendance]) { + firebaseWorker.updateMemberAttendances(memberId: memberId, attendances: attendances) + } + func deleteKakaoTalkUserInfo() { firebaseWorker.deleteKakaoTalkUserInfo() } + + func deleteDocument(id: String) { + firebaseWorker.deleteDocument(id: id) + } } diff --git a/attendance-ios/Source/New/Feature/App/AppCore.swift b/attendance-ios/Source/New/Feature/App/AppCore.swift index fcb5507..652b760 100644 --- a/attendance-ios/Source/New/Feature/App/AppCore.swift +++ b/attendance-ios/Source/New/Feature/App/AppCore.swift @@ -56,13 +56,19 @@ struct App: ReducerProtocol { return .none - case let .path(.element(id: id, action: .signUpName(.pop))): - guard case .some(.signUpName) = state.path[id: id] - else { return .none } - - state.path.pop(from: id) +// case let .path(.element(id: id, action: .signUpName(.pop))): +// guard case .some(.signUpName) = state.path[id: id] +// else { return .none } +// +// state.path.pop(from: id) +// +// return .none - return .none +// case let .path(.popFrom(id: id)): +// guard case .some(.signUpName) = state.path[id: id] +// else { return .none } +// +// return .send(.path(.element(id: id, action: .signUpName(.showCancelPopup)))) case let .appLaunch(.onboarding(.pushSingUpName(userName))): diff --git a/attendance-ios/Source/New/Feature/AttendanceCode/AttendanceCodeCore.swift b/attendance-ios/Source/New/Feature/AttendanceCode/AttendanceCodeCore.swift new file mode 100644 index 0000000..94003f7 --- /dev/null +++ b/attendance-ios/Source/New/Feature/AttendanceCode/AttendanceCodeCore.swift @@ -0,0 +1,154 @@ +// +// Attendance.swift +// attendance-ios +// +// Created by 이호영 on 2023/10/24. +// + +import Foundation + +import ComposableArchitecture + +struct AttendanceCode: ReducerProtocol { + + struct State: Equatable { + @BindingState var code: String = "" + var isEnabledNextButton: Bool = false + var isFocus: Bool = false + + var firstInputView: Bool = false + var secondInputView: Bool = false + var thirdInputView: Bool = false + var fourthInputView: Bool = false + + var isIncorrectCode: Bool = false + var isConfirmCode: Bool = false + + var session: Session + var member: Member? + + var sessionDate: String { + let dateFormatter = DateFormatter() + let sessionDateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + let date = dateFormatter.date(from: session.date) + sessionDateFormatter.dateFormat = "MM월 dd일" + return sessionDateFormatter.string(from: date ?? Date()) + } + + init( + session: Session, + member: Member? + ) { + self.session = session + self.member = member + } + } + + enum Action: Equatable, BindableAction { + case binding(BindingAction) + + case focus(Bool) + case codeCheck + case incorrectCode + + case setAttendance(Status) + case completeAttendance(Member) + case sendError + + case dismissScene + } + + @Dependency(\.kakaoSign) var kakaoSign + @Dependency(\.memberInfo.memberInfo) var memberInfo + + var body: some ReducerProtocolOf { + BindingReducer() + + Reduce { state, action in + switch action { + case .binding(\.$code): + let count = state.code.count + let maxInputViews = 4 + var inputViews = Array(repeating: false, count: maxInputViews) + state.isEnabledNextButton = count == 4 + + for index in 0.. currenTime { + if endTime > currenTime { + return .send(.setAttendance(.normal)) + } else { + return .send(.setAttendance(.late)) + } + } else { + return .send(.setAttendance(.absent)) + } + + } else { + return .send(.sendError) + } + } else { + return .run { send in + await send(.incorrectCode) + } + } + case let .setAttendance(status): + + if var member = state.member { + let index = state.session.sessionId + member.attendances[index].status = status + + memberInfo.updateMemberAttendances(memberId: member.id, attendances: member.attendances) + + return .send(.completeAttendance(member)) + } else { + return .send(.sendError) + } + case .incorrectCode: + state.isIncorrectCode = true + state.isFocus = false + return .none + default: + return .none + } + } + } +} diff --git a/attendance-ios/Source/New/Feature/AttendanceCode/AttendanceCodeView.swift b/attendance-ios/Source/New/Feature/AttendanceCode/AttendanceCodeView.swift new file mode 100644 index 0000000..543444a --- /dev/null +++ b/attendance-ios/Source/New/Feature/AttendanceCode/AttendanceCodeView.swift @@ -0,0 +1,187 @@ +// +// AttendanceCodeView.swift +// attendance-ios +// +// Created by 이호영 on 2023/10/24. +// + +import SwiftUI + +import ComposableArchitecture + +struct AttendanceCodeView: View { + + let store: StoreOf + + @FocusState private var isFocus: Bool + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + VStack { + + HStack { + + Spacer() + + Button { + viewStore.send(.dismissScene) + } label: { + Image("close") + .resizable() + .tint(.gray_800) + .frame(width: 24, height: 24) + .padding(.trailing, 17) + } + + } + .frame(maxWidth: .infinity) + .frame(height: 56) + + ZStack { + + VStack(spacing: 28) { + VStack(spacing: 20) { + ZStack { + TextField("", text: viewStore.binding(\.$code)) + .focused($isFocus) + .keyboardType(.decimalPad) + .modifier(KeyboardAdaptive()) + .foregroundColor(Color.clear) + .accentColor(Color.clear) + .background(Color.clear) + .onChange(of: isFocus, perform: { newValue in + viewStore.send(.focus(newValue), animation: .default) + }) + .onChange(of: viewStore.isFocus, perform: { value in + isFocus = value + }) + + VStack(spacing: 12) { + YPText( + string: AttributedString("\(viewStore.sessionDate) 세션 출석"), + color: .gray_1200, + font: .YPHead1 + ) + .frame(maxWidth: .infinity, alignment: .leading) + + YPText( + string: "암호 코드 4자리를 입력해주세요", + color: .gray_800, + font: .YPBody1 + ) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + } + .background(Color.background) + } + .onAppear { + isFocus = true + } + + HStack(spacing: 13) { + + Button { + isFocus = true + } label: { + if viewStore.firstInputView { + Image("code_first") + .resizable() + .frame(width: 72, height: 72) + } else { + Rectangle() + .frame(width: 72, height: 72) + .foregroundColor(Color.gray_200) + .cornerRadius(50) + } + } + + Button { + isFocus = true + } label: { + if viewStore.secondInputView { + Image("code_second") + .resizable() + .frame(width: 72, height: 72) + } else { + Rectangle() + .frame(width: 72, height: 72) + .foregroundColor(Color.gray_200) + .cornerRadius(50) + } + } + + Button { + isFocus = true + } label: { + if viewStore.thirdInputView { + Image("code_third") + .resizable() + .frame(width: 72, height: 72) + } else { + Rectangle() + .frame(width: 72, height: 72) + .foregroundColor(Color.gray_200) + .cornerRadius(50) + } + } + + Button { + isFocus = true + } label: { + if viewStore.fourthInputView { + Image("code_fourth") + .resizable() + .frame(width: 72, height: 72) + } else { + Rectangle() + .frame(width: 72, height: 72) + .foregroundColor(Color.gray_200) + .cornerRadius(50) + } + } + } + .padding(.top, 4) + + if viewStore.isIncorrectCode && viewStore.isConfirmCode { + YPText( + string: "틀린 코드입니다", + color: .yapp_orange, + font: .YPSubHead1 + ) + .frame(maxWidth: .infinity, alignment: .center) + } + + } + .padding(.top, 32) + .padding(.horizontal, 24) + + + VStack { + Spacer() + + if viewStore.isIncorrectCode == false && viewStore.isConfirmCode == false { + Button { + viewStore.send(.codeCheck) + } label: { + YPText( + string: "확인", + color: .white, + font: .YPHead2 + ) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.vertical, 19) + .background(viewStore.isEnabledNextButton ? Color.yapp_orange : Color.gray_400) + .disabled(viewStore.isEnabledNextButton == false) + .cornerRadius(viewStore.isFocus ? 0 : 12) + } + .padding(.horizontal, viewStore.isFocus ? 0 : 24) + .padding(.bottom, viewStore.isFocus ? 0 : 6) + } + } + } + } + } + .navigationBarBackButtonHidden(true) + } + } +} diff --git a/attendance-ios/Source/New/Feature/OnBoarding/OnboardingCore.swift b/attendance-ios/Source/New/Feature/OnBoarding/OnboardingCore.swift index 62ae145..f856f8d 100644 --- a/attendance-ios/Source/New/Feature/OnBoarding/OnboardingCore.swift +++ b/attendance-ios/Source/New/Feature/OnBoarding/OnboardingCore.swift @@ -23,7 +23,8 @@ struct Onboarding: ReducerProtocol { enum Action { case launch case kakaoSignButtonTapped - case appleSignButtonTapped + case registerAppleSign + case appleSignButtonTapped(name: String) case compareKakaoUserId(String) case pushSingUpName(String) @@ -65,12 +66,26 @@ struct Onboarding: ReducerProtocol { await send(.pushSingUpName("")) } - case .appleSignButtonTapped: + case let .appleSignButtonTapped(name): return .run { send in - let name = try await appleSign.login() - await send(.pushSingUpName(name)) + let userId = try await KeyChainManager.shared.read(account: .userId) + let platform = try await KeyChainManager.shared.read(account: .platform) + + if platform == "apple" { + let member = try await memberInfo.memberInfo.getMemberInfo(memberId: Int(userId) ?? 0) + + try await KeyChainManager.shared.create(account: .userId, data: userId) + try await KeyChainManager.shared.create(account: .platform, data: "apple") + await send(.pushHomeScene(member)) + } + } catch: { error, send in - // + await send(.registerAppleSign) + await send(.pushSingUpName(name)) + } + case .registerAppleSign: + return .run { send in + try await KeyChainManager.shared.create(account: .platform, data: "apple") } default: diff --git a/attendance-ios/Source/New/Feature/OnBoarding/OnboardingView.swift b/attendance-ios/Source/New/Feature/OnBoarding/OnboardingView.swift index 375955e..7b54926 100644 --- a/attendance-ios/Source/New/Feature/OnBoarding/OnboardingView.swift +++ b/attendance-ios/Source/New/Feature/OnBoarding/OnboardingView.swift @@ -7,93 +7,110 @@ import SwiftUI +import AuthenticationServices + import ComposableArchitecture struct OnboardingView: View { - - let store: StoreOf - - var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - ZStack { - if viewStore.isFirstLaunched { - LottieView(filename: "splash_main") { _ in - viewStore.send(.launch) - } - } else { - VStack { - Image("splash_main_still") - .frame(width: 375, height: 375) - .padding(.top, 120) - - Spacer() + + let store: StoreOf + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + ZStack { + if viewStore.isFirstLaunched { + LottieView(filename: "splash_main") { _ in + viewStore.send(.launch) + } + } else { + VStack { + Image("splash_main_still") + .frame(width: 375, height: 375) + .padding(.top, 120) + + Spacer() + } + } + + if viewStore.isLaunching || viewStore.isFirstLaunched == false { + + VStack(spacing: 8) { + Spacer() + + YPText( + string: "3초만에 끝나는 \n간편한 출석체크", + color: .gray_1200, + font: .YPHead1 + ) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 68) + + SignInWithAppleButton( + onRequest: { request in + request.requestedScopes = [.fullName] + }, + onCompletion: { result in + switch result { + case .success(let authResults): + switch authResults.credential{ + case let appleIDCredential as ASAuthorizationAppleIDCredential: + let userId = appleIDCredential.user + let fullName = appleIDCredential.fullName + let name = (fullName?.familyName ?? "") + (fullName?.givenName ?? "") + + print(userId) + print(name) + viewStore.send(.appleSignButtonTapped(name: name)) + default: + break + } + case .failure(let error): + print(error.localizedDescription) + print("error") } } + ) + .frame(maxWidth: .infinity, alignment: .center) + .frame(height: 44) + .cornerRadius(12) + + Button { + viewStore.send(.kakaoSignButtonTapped) + } label: { + HStack(spacing: 6) { + Image("kakao_login") - if viewStore.isLaunching || viewStore.isFirstLaunched == false { - - VStack(spacing: 8) { - Spacer() - - YPText( - string: "3초만에 끝나는 \n간편한 출석체크", - color: .gray_1200, - font: .YPHead1 - ) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.bottom, 68) - - Button { - viewStore.send(.appleSignButtonTapped) - } label: { - YPText( - string: "Apple로 로그인", - color: .white, - font: .YPFont(type: .regular, size: 19) - ) - .frame(maxWidth: .infinity, alignment: .center) - .padding(.vertical, 10) - .background(Color.black) - .cornerRadius(12) - } - - Button { - viewStore.send(.kakaoSignButtonTapped) - } label: { - HStack(spacing: 6) { - Image("kakao_login") - - YPText( - string: "카카오 로그인", - color: .gray_1200, - font: .YPFont(type: .regular, size: 19) - ) - } - .frame(maxWidth: .infinity, alignment: .center) - .padding(.vertical, 10) - .background(Color.etc_yellow) - .cornerRadius(12) - } - - } - .frame(maxWidth: .infinity, alignment: .center) - .padding(.horizontal, 24) - .padding(.bottom, 47) - } + YPText( + string: "카카오 로그인", + color: .gray_1200, + font: .YPFont(type: .regular, size: 19) + ) + } + .frame(maxWidth: .infinity, alignment: .center) + .padding(.vertical, 10) + .background(Color.etc_yellow) + .cornerRadius(12) } - .background(viewStore.isLaunching || viewStore.isFirstLaunched == false ? Color.white : Color.yapp_orange) + + } + .frame(maxWidth: .infinity, alignment: .center) + .padding(.horizontal, 24) + .padding(.bottom, 47) } + } + .background(viewStore.isLaunching || viewStore.isFirstLaunched == false ? Color.white : Color.yapp_orange) } + } } struct OnboardingView_Previews: PreviewProvider { - static var previews: some View { - OnboardingView( - store: .init( - initialState: Onboarding.State(), - reducer: Onboarding() - ) - ) - } + static var previews: some View { + OnboardingView( + store: .init( + initialState: Onboarding.State(), + reducer: Onboarding() + ) + ) + } } diff --git a/attendance-ios/Source/New/Feature/ScoreCheck/ScoreChartCore.swift b/attendance-ios/Source/New/Feature/ScoreCheck/ScoreChartCore.swift index 1bc99f9..20869fb 100644 --- a/attendance-ios/Source/New/Feature/ScoreCheck/ScoreChartCore.swift +++ b/attendance-ios/Source/New/Feature/ScoreCheck/ScoreChartCore.swift @@ -29,9 +29,10 @@ struct ScoreChart: ReducerProtocol { Reduce { state, action in switch action { case let .setStatusList(status): - state.score = status.map { $0.point }.reduce(0) { $0 + $1 } + let score = 100 + status.map { $0.point }.reduce(0) { $0 + $1 } + state.score = score > 0 ? score : 0 - state.attendanceCount = status.filter { $0 == .admit && $0 == .normal }.count + state.attendanceCount = status.filter { $0 == .admit || $0 == .normal }.count state.lateCount = status.filter { $0 == .late }.count state.absentCount = status.filter { $0 == .absent }.count diff --git a/attendance-ios/Source/New/Feature/ScoreCheck/ScoreCheckCore.swift b/attendance-ios/Source/New/Feature/ScoreCheck/ScoreCheckCore.swift index 5750e67..949bb16 100644 --- a/attendance-ios/Source/New/Feature/ScoreCheck/ScoreCheckCore.swift +++ b/attendance-ios/Source/New/Feature/ScoreCheck/ScoreCheckCore.swift @@ -25,7 +25,7 @@ struct ScoreCheck: ReducerProtocol { var sessionList: IdentifiedArrayOf = [] var scoreChart = ScoreChart.State() - let member: Member? + var member: Member? init(member: Member?) { self.member = member @@ -38,6 +38,7 @@ struct ScoreCheck: ReducerProtocol { case onAppear case setSessionList([Session]) + case setMember(Member?) } @Dependency(\.sessionInfo.sessionInfo) var sessionInfo @@ -51,6 +52,13 @@ struct ScoreCheck: ReducerProtocol { await send(.setSessionList(sessions)) } + + case let .setMember(member): + + state.member = member + + return .none + case let .setSessionList(sessions): guard let member = state.member else { return .none @@ -63,7 +71,7 @@ struct ScoreCheck: ReducerProtocol { if session.type == .dontNeedAttendance { state.sessionList.updateOrAppend(.init(session: session, status: .notNeed)) - } else if Date().isPast(than: session.date.date()) == false { + } else if Date().isPastBeforeFiveMinuate(than: session.date.date()) == false { state.sessionList.updateOrAppend(.init(session: session, status: .pre)) } else { state.sessionList.updateOrAppend(.init(session: session, status: attendance.status.convertSessionStatus())) diff --git a/attendance-ios/Source/New/Feature/Setting/SettingCore.swift b/attendance-ios/Source/New/Feature/Setting/SettingCore.swift index d793a33..e555169 100644 --- a/attendance-ios/Source/New/Feature/Setting/SettingCore.swift +++ b/attendance-ios/Source/New/Feature/Setting/SettingCore.swift @@ -20,6 +20,7 @@ struct Setting: ReducerProtocol { } @PresentationState var destination: Destination.State? + @BindingState var isShowDeletePopup: Bool = false init(member: Member?) { self.member = member @@ -31,7 +32,7 @@ struct Setting: ReducerProtocol { } } - enum Action: Equatable { + enum Action: Equatable, BindableAction { case onAppear case setYappGeneration(Int) case tappedTeamSelect @@ -41,7 +42,11 @@ struct Setting: ReducerProtocol { case logout case deleteUser + case showDeletePopup + case dismissDeletePopup + case destination(PresentationAction) + case binding(BindingAction) } public struct Destination: ReducerProtocol { @@ -71,6 +76,8 @@ struct Setting: ReducerProtocol { @Dependency(\.kakaoSign) var kakaoSign var body: some ReducerProtocolOf { + BindingReducer() + Reduce { state, action in switch action { case .onAppear: @@ -108,10 +115,11 @@ struct Setting: ReducerProtocol { case .logout: return .run { send in let platform = try await KeyChainManager.shared.read(account: .platform) - try await KeyChainManager.shared.delete(account: .userId) - try await KeyChainManager.shared.delete(account: .platform) + if platform == "kakao" { + try await KeyChainManager.shared.delete(account: .userId) + try await KeyChainManager.shared.delete(account: .platform) try await kakaoSign.logout() } } @@ -119,15 +127,24 @@ struct Setting: ReducerProtocol { return .run { send in let platform = try await KeyChainManager.shared.read(account: .platform) + let userId = try await KeyChainManager.shared.read(account: .userId) try await KeyChainManager.shared.delete(account: .userId) try await KeyChainManager.shared.delete(account: .platform) if platform == "kakao" { memberInfo.deleteKakaoTalkUserInfo() try await kakaoSign.logout() + } else { + memberInfo.deleteDocument(id: userId) } } + case .showDeletePopup: + state.isShowDeletePopup = true + return .none + case .dismissDeletePopup: + state.isShowDeletePopup = false + return .none default: return .none } diff --git a/attendance-ios/Source/New/Feature/Setting/SettingView.swift b/attendance-ios/Source/New/Feature/Setting/SettingView.swift index 147448c..9328e15 100644 --- a/attendance-ios/Source/New/Feature/Setting/SettingView.swift +++ b/attendance-ios/Source/New/Feature/Setting/SettingView.swift @@ -138,7 +138,7 @@ struct SettingView: View { } Button { - viewStore.send(.deleteUser) + viewStore.send(.showDeletePopup) } label: { HStack { YPText( @@ -164,7 +164,7 @@ struct SettingView: View { ) { store in TeamSelectView(store: store) } - .popup(isPresented: viewStore.binding(\.$showingCancelPopup)) { + .popup(isPresented: viewStore.binding(\.$isShowDeletePopup)) { VStack { VStack(spacing: 19) { VStack(spacing: 8) { @@ -185,10 +185,10 @@ struct SettingView: View { HStack(spacing: 8) { Button { - viewStore.send(.pop) + viewStore.send(.deleteUser) } label: { YPText( - string: "취소", + string: "탈퇴하기", color: .gray_800, font: .YPSubHead1 ) @@ -200,10 +200,10 @@ struct SettingView: View { .frame(maxWidth: .infinity) Button { - viewStore.send(.dismissCancelPopup) + viewStore.send(.dismissDeletePopup) } label: { YPText( - string: "탈퇴합니다", + string: "취소", color: .white, font: .YPSubHead1 ) diff --git a/attendance-ios/Source/New/Feature/SignUp/SignUpCodeCore.swift b/attendance-ios/Source/New/Feature/SignUp/SignUpCodeCore.swift index 112131b..13a9723 100644 --- a/attendance-ios/Source/New/Feature/SignUp/SignUpCodeCore.swift +++ b/attendance-ios/Source/New/Feature/SignUp/SignUpCodeCore.swift @@ -84,10 +84,10 @@ struct SignUpCode: ReducerProtocol { let selectedPosition = state.selectedPosition if state.code == "1234" { return .run { send in - try await kakaoSign.saveUserId() - let userID = try await KeyChainManager.shared.read(account: .userId) let platform = try await KeyChainManager.shared.read(account: .platform) if platform == "kakao" { + try await kakaoSign.saveUserId() + let userID = try await KeyChainManager.shared.read(account: .userId) try await memberInfo.registerKakaoUser( memberId: Int(userID) ?? 0, newUserInfo: .init( @@ -100,7 +100,22 @@ struct SignUpCode: ReducerProtocol { let member = try await memberInfo.getMemberInfo(memberId: Int(userID) ?? 0) await send(.pushHomeTab(member)) } else { - //apple + let userID = Int.random(in: 1000000000..<10000000000) + + try await KeyChainManager.shared.create(account: .userId, data: String(userID)) + try await KeyChainManager.shared.create(account: .platform, data: "apple") + + try await memberInfo.registerKakaoUser( + memberId: userID, + newUserInfo: .init( + name: name, + positionType: selectedPosition, + teamType: .none, + teamNumber: 0 + ) + ) + let member = try await memberInfo.getMemberInfo(memberId: userID) + await send(.pushHomeTab(member)) } } catch: { error, send in print(error) diff --git a/attendance-ios/Source/New/Feature/Tab/HomeTabCore.swift b/attendance-ios/Source/New/Feature/Tab/HomeTabCore.swift index 75ae87b..fd94dd9 100644 --- a/attendance-ios/Source/New/Feature/Tab/HomeTabCore.swift +++ b/attendance-ios/Source/New/Feature/Tab/HomeTabCore.swift @@ -20,6 +20,11 @@ struct HomeTab: ReducerProtocol { @BindingState var selectedTab: Tab + @PresentationState var destination: Destination.State? + + @BindingState var isShowToast: Bool = false + var toastMessage: String = "" + var member: Member? init(member: Member?, selectTab: Tab) { @@ -32,13 +37,45 @@ struct HomeTab: ReducerProtocol { enum Action: BindableAction { case binding(BindingAction) + case destination(PresentationAction) case todaySession(TodaySession.Action) case scoreCheck(ScoreCheck.Action) case tappedSettingButton(Member?) case setMember(Member?) + + case tappedAttendanceButton + case attendanceCheck(Session?) + case attendanceTimeCheck(Session?) + + case showToast(String) + case showAttendanceCode(Session) + } + + public struct Destination: ReducerProtocol { + public enum State: Equatable { + case attendanceCode(AttendanceCode.State) + case alert(AlertState) + } + + public enum Action: Equatable { + case attendanceCode(AttendanceCode.Action) + case alert(Alert) + + public enum Alert { + case cancel + } + } + + public var body: some ReducerProtocolOf { + Scope(state: /State.attendanceCode, action: /Action.attendanceCode) { + AttendanceCode() + } + } } + @Dependency(\.sessionInfo.sessionInfo) var sessionInfo + var body: some ReducerProtocolOf { BindingReducer() @@ -49,10 +86,100 @@ struct HomeTab: ReducerProtocol { case let .setMember(member): state.member = member return .none + case .tappedAttendanceButton: + + return .run { send in + let session = try await sessionInfo.todaySession() + + await send(.attendanceCheck(session)) + + } catch: { error, send in + await send(.showToast("에러가 발생했습니다.")) + } + case let .attendanceCheck(session): + var isAttendanced: Bool = true + if let session = session, + let member = state.member { + member.attendances.forEach { + if $0.sessionId == session.sessionId { + if $0.status == .absent { + isAttendanced = false + } else { + isAttendanced = true + } + } + } + } + + if isAttendanced == false { + return .send(.attendanceTimeCheck(session)) + } else { + return .send(.showToast("이미 출석을 완료했어요.")) + } + + case let .attendanceTimeCheck(session): + + let format = DateFormatter() + format.dateFormat = "yyyy-MM-dd HH:mm:ss" + format.locale = Locale(identifier: "ko_KR") + format.timeZone = TimeZone(abbreviation: "KST") + + let calendar = Calendar.current + let currenTime = Date() + + if let session = session, + let sessionTime = format.date(from: session.date), + let startTime = calendar.date(byAdding: .minute, value: -5, to: sessionTime), + let endTime = calendar.date(byAdding: .minute, value: 30, to: sessionTime), + startTime <= currenTime, + endTime > currenTime + { + return .send(.showAttendanceCode(session)) + } else { + return .send(.showToast("지금은 출석할 수 없어요.")) + } + case let .showToast(message): + + state.toastMessage = message + state.isShowToast = true + + return .none + + case let .showAttendanceCode(session): + + state.destination = .attendanceCode(AttendanceCode.State(session: session, member: state.member)) + + return .none + + case .destination(.presented(.attendanceCode(.dismissScene))): + + state.destination = nil + + return .none + + case let .destination(.presented(.attendanceCode(.completeAttendance(member)))): + + state.destination = nil + state.member = member + + return .run { send in + await send(.todaySession(.setMember(member))) + await send(.scoreCheck(.setMember(member))) + } + + case .destination(.presented(.attendanceCode(.sendError))): + + state.destination = nil + + return .send(.showToast("에러가 발생했습니다.")) + default: return .none } } + .ifLet(\.$destination, action: /Action.destination) { + Destination() + } Scope(state: \.todaySession, action: /Action.todaySession) { TodaySession() diff --git a/attendance-ios/Source/New/Feature/Tab/HomeTabView.swift b/attendance-ios/Source/New/Feature/Tab/HomeTabView.swift index 4c93f1d..ee60572 100644 --- a/attendance-ios/Source/New/Feature/Tab/HomeTabView.swift +++ b/attendance-ios/Source/New/Feature/Tab/HomeTabView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import PopupView import ComposableArchitecture @@ -15,64 +16,113 @@ struct HomeTabView: View { var body: some View { WithViewStore(self.store, observe: { $0 }) { viewStore in - TabView(selection: viewStore.binding(\.$selectedTab)) { - TodaySessionView( - store: self.store.scope( - state: \.todaySession, - action: HomeTab.Action.todaySession - ) - ) - .tabItem({ - VStack(spacing: 4) { - Image("home_disabled") - - Text("오늘 세션") - } - }) - .tag(HomeTab.Tab.todaySession) + ZStack(alignment: .bottom) { - ScoreCheckView( - store: self.store.scope( - state: \.scoreCheck, - action: HomeTab.Action.scoreCheck + TabView(selection: viewStore.binding(\.$selectedTab)) { + TodaySessionView( + store: self.store.scope( + state: \.todaySession, + action: HomeTab.Action.todaySession + ) ) - ) - .tabItem({ - VStack(spacing: 4) { - Image("check_disabled") - - Text("출결 확인") - } - }) - .tag(HomeTab.Tab.scoreCheck) - } - .navigationBarBackButtonHidden(true) - .navigationBarTitleDisplayMode(.inline) - .applyIf(viewStore.selectedTab == .todaySession, apply: { - $0 - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button { - viewStore.send(.tappedSettingButton(viewStore.member)) - } label: { - Image("setting") - .foregroundColor(Color.gray_600) + .tabItem({ + VStack(spacing: 4) { + Image("home_disabled") + + Text("오늘 세션") + } + .frame(width: 100) + }) + .tag(HomeTab.Tab.todaySession) + + ScoreCheckView( + store: self.store.scope( + state: \.scoreCheck, + action: HomeTab.Action.scoreCheck + ) + ) + .tabItem({ + VStack(spacing: 4) { + Image("check_disabled") + + Text("출결 확인") + } + .frame(width: 100) + }) + .tag(HomeTab.Tab.scoreCheck) + } + .navigationBarBackButtonHidden(true) + .navigationBarTitleDisplayMode(.inline) + .applyIf(viewStore.selectedTab == .todaySession, apply: { + $0 + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + viewStore.send(.tappedSettingButton(viewStore.member)) + } label: { + Image("setting") + .foregroundColor(Color.gray_600) + } } } + .toolbarBackground( + Color.gray_200, + for: .navigationBar + ) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("") + .font(Font.YPFont(type: .medium, size: 18)) + .foregroundColor(Color.gray_1200) + }) + .applyIf(viewStore.selectedTab == .scoreCheck, apply: { + $0 + .navigationTitle("출결 점수 확인") + }) + + Button { + viewStore.send(.tappedAttendanceButton) + } label: { + + VStack { + Image("qr_home") + .frame(width: 45, height: 45) + .background(Color.yapp_orange) + .clipShape(Circle()) } - .toolbarBackground( - Color.gray_200, - for: .navigationBar + } + } + .fullScreenCover( + store: self.store.scope(state: \.$destination, action: { .destination($0) }), + state: /HomeTab.Destination.State.attendanceCode, + action: HomeTab.Destination.Action.attendanceCode + ) { store in + AttendanceCodeView(store: store) + } + .popup(isPresented: viewStore.$isShowToast) { + VStack { + YPText( + string: AttributedString(viewStore.toastMessage), + color: .white, + font: .YPBody1 ) - .toolbarBackground(.visible, for: .navigationBar) - .navigationTitle("") - .font(Font.YPFont(type: .medium, size: 18)) - .foregroundColor(Color.gray_1200) - }) - .applyIf(viewStore.selectedTab == .scoreCheck, apply: { - $0 - .navigationTitle("출결 점수 확인") - }) + .padding(.vertical, 12) + .frame(maxWidth: .infinity, alignment: .center) + + } + .background(Color(red: 0.173, green: 0.185, blue: 0.208, opacity: 0.8)) + .cornerRadius(10) + .padding(.horizontal, 24) + .padding(.bottom, 50) + + } customize: { + $0 + .type(.floater()) + .position(.bottom) + .animation(.spring()) + .closeOnTapOutside(true) + .autohideIn(2) + } + } } } diff --git a/attendance-ios/Source/New/Feature/TeamSelect/TeamSelectView.swift b/attendance-ios/Source/New/Feature/TeamSelect/TeamSelectView.swift index e4e8177..490159d 100644 --- a/attendance-ios/Source/New/Feature/TeamSelect/TeamSelectView.swift +++ b/attendance-ios/Source/New/Feature/TeamSelect/TeamSelectView.swift @@ -17,6 +17,9 @@ struct TeamSelectView: View { WithViewStore(self.store, observe: { $0 }) { viewStore in VStack { HStack { + + Spacer() + Button { viewStore.send(.dismiss) } label: { @@ -24,10 +27,9 @@ struct TeamSelectView: View { .resizable() .tint(.gray_800) .frame(width: 24, height: 24) - .padding(.leading, 17) + .padding(.trailing, 17) } - Spacer() } .frame(maxWidth: .infinity) .frame(height: 56) diff --git a/attendance-ios/Source/New/Feature/TodaySession/TodaySessionCore.swift b/attendance-ios/Source/New/Feature/TodaySession/TodaySessionCore.swift index c261f1b..09a6193 100644 --- a/attendance-ios/Source/New/Feature/TodaySession/TodaySessionCore.swift +++ b/attendance-ios/Source/New/Feature/TodaySession/TodaySessionCore.swift @@ -44,6 +44,8 @@ struct TodaySession: ReducerProtocol { case setSession(Session?) case setMember(Member?) case pushSetting(Member?) + + case setAttendance } @Dependency(\.sessionInfo.sessionInfo) var sessionInfo @@ -60,9 +62,23 @@ struct TodaySession: ReducerProtocol { } case let .setSession(currentSession): state.session = currentSession - return .none + return .send(.setAttendance) case let .setMember(member): state.member = member + return .send(.setAttendance) + + case .setAttendance: + + if let session = state.session, + let member = state.member { + print(member.attendances) + if member.attendances[session.sessionId].status == .absent { + state.isCompleteAttendance = false + } else { + state.isCompleteAttendance = true + } + } + return .none default: return .none diff --git a/attendance-ios/Source/New/Feature/TodaySession/TodaySessionView.swift b/attendance-ios/Source/New/Feature/TodaySession/TodaySessionView.swift index 3178663..195de78 100644 --- a/attendance-ios/Source/New/Feature/TodaySession/TodaySessionView.swift +++ b/attendance-ios/Source/New/Feature/TodaySession/TodaySessionView.swift @@ -25,18 +25,22 @@ struct TodaySessionView: View { Color.gray_200 .frame(height: 500) - Image("illust_member_home_disabled") + Image(viewStore.isCompleteAttendance ? "illust_member_home_enabled" : "illust_member_home_disabled") .resizable() .frame(width: 375, height: 88) VStack { HStack(spacing: 8) { - Image("info_check_disabled") + Image(viewStore.isCompleteAttendance ? "qr_check_enabled" : "info_check_disabled") .resizable() .frame(width: 20, height: 20) - YPText(string: "아직 출석 전이에요", color: .gray_600, font: .YPBody1) - .frame(maxWidth: .infinity, alignment: .leading) + YPText( + string: viewStore.isCompleteAttendance ? "출석을 완료했어요": "아직 출석 전이에요", + color: viewStore.isCompleteAttendance ? .yapp_orange : .gray_600, + font: .YPBody1 + ) + .frame(maxWidth: .infinity, alignment: .leading) } .padding(.all, 20) }