diff --git a/.github/workflows/Washer_CI.yml b/.github/workflows/Washer_CI.yml index 72d5cfa..37dc460 100644 --- a/.github/workflows/Washer_CI.yml +++ b/.github/workflows/Washer_CI.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Swift - uses: swift-actions/setup-swift@v2 + uses: swift-actions/setup-swift@main with: swift-version: '5.9' diff --git a/Projects/App/Sources/DesignSystem/Component/Button/WasherButton.swift b/Projects/App/Sources/DesignSystem/Component/Button/WasherButton.swift index abd608b..5203cf3 100644 --- a/Projects/App/Sources/DesignSystem/Component/Button/WasherButton.swift +++ b/Projects/App/Sources/DesignSystem/Component/Button/WasherButton.swift @@ -32,18 +32,17 @@ public struct WasherButton: View { Button(action: { self.action() }) { - Text(text) - .font(.pretendard(.semiBold, size: 14)) - .color(.gray50) - .padding(.vertical, verticalPadding) - .padding(.horizontal, horizontalPadding) - .background( - RoundedRectangle(cornerRadius: 30) - .fill(isPressed ? Color.color(.main100) : Color.color(.main100)) - ) - .scaleEffect(isPressed ? 0.9 : 1.0) - + ZStack { + RoundedRectangle(cornerRadius: 8) + .fill(isPressed ? Color.color(.main100) : Color.color(.main100)) + Text(text) + .font(.pretendard(.semiBold, size: 14)) + .color(.gray50) + } + .padding(.horizontal, horizontalPadding) + .frame(height: 44) + .scaleEffect(isPressed ? 0.9 : 1.0) } .buttonStyle(PlainButtonStyle()) .gesture( diff --git a/Projects/App/Sources/DesignSystem/Component/TextField/WasherTextField.swift b/Projects/App/Sources/DesignSystem/Component/TextField/WasherTextField.swift index 74f360a..22f4b7d 100644 --- a/Projects/App/Sources/DesignSystem/Component/TextField/WasherTextField.swift +++ b/Projects/App/Sources/DesignSystem/Component/TextField/WasherTextField.swift @@ -63,11 +63,11 @@ struct WasherTextField: View { } } .padding(.horizontal, 16) - .frame(height: 44) + .frame(height: 42) .onSubmit(onSubmit) .focused($isFocused) .foregroundColor(.color(.gray700)) - .font(.pretendard(.semiBold, size: 14)) + .font(.pretendard(.semiBold, size: 12)) if isSecure { Button { @@ -96,9 +96,9 @@ struct WasherTextField: View { if isError { Text(errorText) .foregroundStyle(.red) - .font(.pretendard(.regular, size: 12)) + .font(.pretendard(.regular, size: 11)) } } - .padding(.horizontal, 16) + //.padding(.horizontal, 16) } } diff --git a/Projects/App/Sources/DesignSystem/DesignModifire.swift b/Projects/App/Sources/DesignSystem/DesignModifire.swift index 44bba90..76f83d8 100644 --- a/Projects/App/Sources/DesignSystem/DesignModifire.swift +++ b/Projects/App/Sources/DesignSystem/DesignModifire.swift @@ -66,7 +66,7 @@ struct DesignModifireView: View { var body: some View { Text("Hello, Pretendard!") .font(.pretendard(.semiBold, size: 20)) - .color(.gray600) + .color(.gray200) } } diff --git a/Projects/App/Sources/Extension/Validator.swift b/Projects/App/Sources/Extension/Validator.swift index 17c94ab..f8456c1 100644 --- a/Projects/App/Sources/Extension/Validator.swift +++ b/Projects/App/Sources/Extension/Validator.swift @@ -22,3 +22,9 @@ struct Validator { .evaluate(with: password) } } + +extension Character { + var isHangul: Bool { + return ("가"..."힣").contains(self) + } +} diff --git a/Projects/App/Sources/Feature/AuthFeature/InfoInputFeature/View/InfoInputView.swift b/Projects/App/Sources/Feature/AuthFeature/InfoInputFeature/View/InfoInputView.swift index f693590..0368bae 100644 --- a/Projects/App/Sources/Feature/AuthFeature/InfoInputFeature/View/InfoInputView.swift +++ b/Projects/App/Sources/Feature/AuthFeature/InfoInputFeature/View/InfoInputView.swift @@ -9,11 +9,155 @@ import SwiftUI struct InfoInputView: View { + @StateObject var authViewModel: AuthViewModel + @State var nameTextField: String = "" + @State var nameIsError: Bool = false + @State var schoolNumberTextField: String = "" + @State var schoolNumberIsError: Bool = false + @State var domitoryRoomTextField: String = "" + @State var domitoryRoomIsError: Bool = false + @State var selectedGender: String = "남자" + let genders = ["남자", "여자"] + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + VStack(spacing: 0) { + ZStack { + HStack { + WasherAsset.washerLeftButton.swiftUIImage + .padding(.leading, 26) + Spacer() + } + + VStack(spacing: 6) { + Text("회원가입") + .font(.pretendard(.bold, size: 18)) + .foregroundStyle(.black) + + Text("로그인 시 사용할 정보를 입력해주세요") + .font(.pretendard(.regular, size: 12)) + .color(.gray300) + } + } + .padding(.top, 50) + + WasherTextField( + "이름을 입력해주세요", + text: $nameTextField, + title: "이름", + errorText: "이름은 한글 2~4자여야 합니다.", + isError: nameIsError + ) + .padding(.top, 38) + .onChange(of: nameTextField) { newValue in + nameTextField = String(newValue.prefix(4)) + + let isOnlyHangul = nameTextField.allSatisfy { $0.isHangul } + nameIsError = !(isOnlyHangul && (2...4).contains(nameTextField.count)) + } + .padding(.horizontal, 26) + .padding(.top, 34) + + WasherTextField( + "학번을 입력해주세요", + text: $schoolNumberTextField, + title: "학번", + errorText: "형식이 올바르지 않습니다. (예: 3314)", + isError: schoolNumberIsError + ) + .onChange(of: schoolNumberTextField) { newValue in + schoolNumberTextField = String(newValue.prefix(4)).filter { $0.isNumber } + schoolNumberIsError = !isValidSchoolNumber(schoolNumberTextField) + } + .padding(.horizontal, 26) + .padding(.top, 34) + + WasherTextField( + "기숙사 호실을 입력해주세요", + text: $domitoryRoomTextField, + title: "호실", + errorText: "형식이 올바르지 않습니다. (예: 415)", + isError: domitoryRoomIsError + ) + .onChange(of: domitoryRoomTextField) { newValue in + domitoryRoomTextField = String(newValue.prefix(3)).filter { $0.isNumber } + domitoryRoomIsError = !isValidRoomNumber(domitoryRoomTextField) + } + .padding(.horizontal, 26) + .padding(.top, 34) + + HStack { + Text("성별") + .font(.pretendard(.bold, size: 14)) + .color(.main100) + .padding(.top, 34) + + Spacer() + } + .padding(.leading, 26) + + HStack(spacing: 4) { + ForEach(genders, id: \.self) { gender in + ZStack { + RoundedRectangle(cornerRadius: 8) + .fill(selectedGender == gender ? Color.color(.main300) : Color.color(.gray50)) + + Text(gender) + .font(.pretendard(.semiBold, size: 14)) + .color(.gray700) + } + .frame(height: 42) + .onTapGesture { + selectedGender = gender + Haptic.impact(style: .soft) + } + } + } + .padding(.horizontal, 26) + .padding(.top, 8) + + Spacer() + + Rectangle() + .frame(height: 2) + .color(.gray50) + + WasherButton( + text: "다음", + horizontalPadding: 26 + ) {} + .disabled(!isFormValid) + .padding(.top, 10) + .padding(.bottom, 30) + } + } + + // MARK: - 정규식 검사 함수들 + + func isValidName(_ name: String) -> Bool { + let regex = #"^[가-힣]{2,4}$"# + return name.range(of: regex, options: .regularExpression) != nil + } + + func isValidSchoolNumber(_ number: String) -> Bool { + let regex = #"^[1-3][1-4](0[1-9]|1[0-8])$"# + return number.range(of: regex, options: .regularExpression) != nil + } + + func isValidRoomNumber(_ number: String) -> Bool { + let regex = #"^[2-5](0[0-9]|1[0-9]|20)$"# + return number.range(of: regex, options: .regularExpression) != nil + } + + var isFormValid: Bool { + return isValidName(nameTextField) && + isValidSchoolNumber(schoolNumberTextField) && + isValidRoomNumber(domitoryRoomTextField) && + !nameTextField.isEmpty && + !schoolNumberTextField.isEmpty && + !domitoryRoomTextField.isEmpty } } #Preview { - InfoInputView() + InfoInputView(authViewModel: AuthViewModel()) } diff --git a/Projects/App/Sources/Feature/AuthFeature/SignInFeature/View/SignInView.swift b/Projects/App/Sources/Feature/AuthFeature/SignInFeature/View/SignInView.swift index a7c4b42..e092f20 100644 --- a/Projects/App/Sources/Feature/AuthFeature/SignInFeature/View/SignInView.swift +++ b/Projects/App/Sources/Feature/AuthFeature/SignInFeature/View/SignInView.swift @@ -47,6 +47,8 @@ struct SignInView: View { errorText: "이메일 형식이 맞지 않습니다.", isError: computedEmailError ) + .padding(.leading, 26) + .padding(.trailing, 8) Text("@") .font(.pretendard(.medium, size: 18)) @@ -54,7 +56,7 @@ struct SignInView: View { .padding(.top, 32) Text("gsm.hs.kr") - .font(.pretendard(.medium, size: 14)) + .font(.pretendard(.medium, size: 12)) .color(.gray400) .padding(.horizontal, 16) .frame(height: 44) @@ -62,7 +64,8 @@ struct SignInView: View { RoundedRectangle(cornerRadius: 8) .color(.gray50) ) - .padding(.horizontal, 16) + .padding(.horizontal, 8) + .padding(.trailing, 18) .padding(.top, 20) } .padding(.top, 64) @@ -76,6 +79,7 @@ struct SignInView: View { isSecure: true ) .padding(.top, 34) + .padding(.horizontal, 26) Button { isLoggedIn.toggle() @@ -90,14 +94,13 @@ struct SignInView: View { Spacer() } - .padding(.leading, 16) + .padding(.leading, 26) .padding(.top, 6) } WasherButton( text: "로그인", - horizontalPadding: 166, - verticalPadding: 17 + horizontalPadding: 26 ) { authViewModel.setupEmail(email: emailTextField) authViewModel.setupPassword(password: passwordTextField) diff --git a/Projects/App/Sources/Feature/AuthFeature/SignUpFeature/View/SignUpView.swift b/Projects/App/Sources/Feature/AuthFeature/SignUpFeature/View/SignUpView.swift index f79cf83..d095006 100644 --- a/Projects/App/Sources/Feature/AuthFeature/SignUpFeature/View/SignUpView.swift +++ b/Projects/App/Sources/Feature/AuthFeature/SignUpFeature/View/SignUpView.swift @@ -10,42 +10,189 @@ import SwiftUI struct SignUpView: View { @StateObject var authViewModel: AuthViewModel + @State var emailTextField: String = "" + @State var emailIsError: Bool = false + @State var authenticationCodeNumberTextField: String = "" + @State var authenticationCodeIsError: Bool = false + @State var passwordTextField: String = "" + @State var passwordIsError: Bool = false + @State var passwordCheckTextField: String = "" + @State var passwordCheckIsError: Bool = false + @State var authenticationSuccess: Bool = false + var body: some View { - VStack { - Button { - authViewModel.setupEmail(email: "s23053") - authViewModel.setupPassword(password: "washertest1!") - authViewModel.setupName(name: "서지완") - authViewModel.setupGrade(grade: "3") - authViewModel.setupClassRoom(classRoom: "3") - authViewModel.setupNumber(number: "14") - authViewModel.setupGender(gender: "MAN") - authViewModel.setupRoom(room: "415") - authViewModel.signUp { statusCode in - if (200...299).contains(statusCode) { - print("\(statusCode) | 회원가입 성공") - } else { - print("\(statusCode) | 회원가입 실패") - } + VStack(spacing: 0) { + ZStack { + HStack { + WasherAsset.washerLeftButton.swiftUIImage + .padding(.leading, 26) + Spacer() + } + + VStack(spacing: 6) { + Text("회원가입") + .font(.pretendard(.bold, size: 18)) + .foregroundStyle(.black) + + Text("로그인 시 사용할 정보를 입력해주세요") + .font(.pretendard(.regular, size: 12)) + .color(.gray300) } - } label: { - Text("회원가입 테스트 버튼") } + .padding(.top, 50) + + HStack(alignment: .top, spacing: 0) { + WasherTextField( + "이메일을 입력해주세요.", + text: $emailTextField, + title: "이메일", + errorText: "이메일 형식이 맞지 않습니다.", + isError: Validator.hasEmailError(emailTextField) + ) + .padding(.leading, 26) + .padding(.trailing, 8) + + Text("@") + .font(.pretendard(.medium, size: 18)) + .color(.gray400) + .padding(.top, 32) + + Text("gsm.hs.kr") + .font(.pretendard(.medium, size: 12)) + .color(.gray400) + .padding(.horizontal, 16) + .frame(height: 44) + .background( + RoundedRectangle(cornerRadius: 8) + .color(.gray50) + ) + .padding(.horizontal, 8) + .padding(.trailing, 18) + .padding(.top, 20) + } + .padding(.top, 72) + + WasherButton( + text: "인증번호 발송", + horizontalPadding: 26 + ) + .disabled(emailTextField.isEmpty || Validator.hasEmailError(emailTextField)) + .padding(.top, 34) - Button { - authViewModel.setupEmail(email: "s23053") - authViewModel.setupPassword(password: "washertest1!") - authViewModel.signIn { statusCode in - if (200...299).contains(statusCode) { - print("\(statusCode) | 로그인 성공") - } else { - print("\(statusCode) | 로그인 실패") - } + HStack(alignment: .bottom, spacing: 10) { + WasherTextField( + "인증번호를 입력해주세요", + text: $authenticationCodeNumberTextField, + title: "인증코드", + errorText: "비밀번호가 틀렸습니다.", + isError: authenticationCodeIsError + ) + + Button { + + } label: { + Text("확인") + .foregroundStyle(.white) + .font(.pretendard(.semiBold, size: 12)) + .padding(.vertical, 14) + .padding(.horizontal, 35) + .background( + RoundedRectangle(cornerRadius: 4) + .color(.main100) + ) + } + .disabled(!Validator.isValidAuthCode(authenticationCodeNumberTextField)) + .opacity(Validator.isValidAuthCode(authenticationCodeNumberTextField) ? 1.0 : 0.5) + } + .padding(.horizontal, 26) + .padding(.top, 34) + + HStack { + if authenticationSuccess == false { + Text("인증번호가 발송되었습니다 ") + .font(.pretendard(.regular, size: 12)) + .color(.gray400) + .padding(.leading, 26) + .padding(.top, 6) } - } label: { - Text("로그인 테스트 버튼") + + Spacer() + } + + WasherTextField( + "8~16자 영어, 숫자, 특수문자 1개 이상", + text: $passwordTextField, + title: "비밀번호", + errorText: "비밀번호는 8~16자, 특수문자 포함 필수", + isError: Validator.hasPasswordError(passwordTextField), + isSecure: true + ) + .padding(.top, 34) + .padding(.horizontal, 26) + + WasherTextField( + "비밀번호를 다시 입력해주세요", + text: $passwordCheckTextField, + errorText: "비밀번호가 틀렸습니다.", + isError: passwordCheckIsError, + isSecure: true + ) + .onChange(of: passwordCheckTextField) { _ in + passwordCheckIsError = Validator.hasPasswordCheckError(passwordTextField, passwordCheckTextField) } + .padding(.top, 8) + .padding(.horizontal, 26) + + Spacer() + + Rectangle() + .frame(height: 2) + .color(.gray50) + + WasherButton( + text: "완료", + horizontalPadding: 26 + ) {} + .disabled(!Validator.isSignUpFormValid( + email: emailTextField, + password: passwordTextField, + passwordCheck: passwordCheckTextField + )) + .padding(.top, 10) + .padding(.bottom, 30) } } } + +extension Validator { + static func hasEmailError(_ email: String) -> Bool { + !email.isEmpty && !isValidEmail(email) + } + + static func hasPasswordError(_ password: String) -> Bool { + !password.isEmpty && !isValidPassword(password) + } + + static func hasPasswordCheckError(_ password: String, _ passwordCheck: String) -> Bool { + !passwordCheck.isEmpty && password != passwordCheck + } + + static func isSignUpFormValid(email: String, password: String, passwordCheck: String) -> Bool { + isValidEmail(email) && + isValidPassword(password) && + password == passwordCheck && + !email.isEmpty && + !password.isEmpty && + !passwordCheck.isEmpty + } + + static func isValidAuthCode(_ code: String) -> Bool { + let pattern = #"^\d{5}$"# + return code.range(of: pattern, options: .regularExpression) != nil + } +} + +#Preview { + SignUpView(authViewModel: AuthViewModel()) +}