로그인 페이지
This commit is contained in:
@@ -11,4 +11,8 @@ enum AppStep {
|
||||
case splash
|
||||
|
||||
case main
|
||||
|
||||
case signUp
|
||||
|
||||
case findPassword
|
||||
}
|
||||
|
15
SodaLive/Sources/Common/ApiResponse.swift
Normal file
15
SodaLive/Sources/Common/ApiResponse.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// ApiResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/09.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ApiResponse<T: Decodable>: Decodable {
|
||||
let success: Bool
|
||||
let data: T?
|
||||
let message: String?
|
||||
let errorProperty: String?
|
||||
}
|
69
SodaLive/Sources/Common/LoadingView.swift
Normal file
69
SodaLive/Sources/Common/LoadingView.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// LoadingView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/09.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LoadingView: View {
|
||||
@State var index = 0
|
||||
|
||||
@State var timer = Timer.publish(every: 0.35, on: .current, in: .common).autoconnect()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.primary.opacity(0.2)
|
||||
.ignoresSafeArea()
|
||||
|
||||
ZStack {
|
||||
Image("loading_1")
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.opacity(index == 0 ? 1.0 : 0.0)
|
||||
|
||||
Image("loading_2")
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.opacity(index == 1 ? 1.0 : 0.0)
|
||||
|
||||
Image("loading_3")
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.opacity(index == 2 ? 1.0 : 0.0)
|
||||
|
||||
Image("loading_4")
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.opacity(index == 3 ? 1.0 : 0.0)
|
||||
|
||||
Image("loading_5")
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.opacity(index == 4 ? 1.0 : 0.0)
|
||||
}
|
||||
.frame(width: 150, height: 150)
|
||||
.background(Color.white)
|
||||
.cornerRadius(14)
|
||||
.shadow(color: Color.primary.opacity(0.07), radius: 5, x: 5, y: 5)
|
||||
.shadow(color: Color.primary.opacity(0.07), radius: 5, x: -5, y: -5)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.onReceive(timer) { _ in
|
||||
DispatchQueue.main.async {
|
||||
if index == 4 {
|
||||
index = 0
|
||||
} else {
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadingView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LoadingView()
|
||||
}
|
||||
}
|
42
SodaLive/Sources/NavigationBar/HomeNavigationBar.swift
Normal file
42
SodaLive/Sources/NavigationBar/HomeNavigationBar.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// HomeNavigationBar.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/09.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HomeNavigationBar<Content: View>: View {
|
||||
|
||||
let title: String
|
||||
let content: Content
|
||||
|
||||
init(
|
||||
title: String,
|
||||
@ViewBuilder content: () -> Content
|
||||
) {
|
||||
self.title = title
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
|
||||
Spacer()
|
||||
|
||||
content
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(width: screenSize().width, height: 50, alignment: .center)
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeNavigationBar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HomeNavigationBar(title: "홈") {}
|
||||
}
|
||||
}
|
13
SodaLive/Sources/User/Login/LoginRequest.swift
Normal file
13
SodaLive/Sources/User/Login/LoginRequest.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// LoginRequest.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/09.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct LoginRequest: Encodable {
|
||||
let email: String
|
||||
let password: String
|
||||
}
|
16
SodaLive/Sources/User/Login/LoginResponse.swift
Normal file
16
SodaLive/Sources/User/Login/LoginResponse.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// LoginResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/09.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct LoginResponse: Decodable {
|
||||
let userId: Int
|
||||
let token: String
|
||||
let nickname: String
|
||||
let email: String
|
||||
let profileImage: String
|
||||
}
|
84
SodaLive/Sources/User/Login/LoginView.swift
Normal file
84
SodaLive/Sources/User/Login/LoginView.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// LoginView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/09.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import PopupView
|
||||
|
||||
struct LoginView: View {
|
||||
|
||||
@ObservedObject var viewModel = LoginViewModel()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.black.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
HomeNavigationBar(title: "로그인") {}
|
||||
|
||||
Spacer()
|
||||
|
||||
UserTextField(
|
||||
title: "이메일",
|
||||
hint: "이메일 주소를 입력해 주세요",
|
||||
isSecure: false,
|
||||
variable: $viewModel.email,
|
||||
keyboardType: .emailAddress
|
||||
)
|
||||
.padding(.horizontal, 26.7)
|
||||
|
||||
UserTextField(
|
||||
title: "비밀번호",
|
||||
hint: "비밀번호를 입력해 주세요",
|
||||
isSecure: true,
|
||||
variable: $viewModel.password,
|
||||
isPasswordVisibleButton: true
|
||||
)
|
||||
.padding(.top, 33.3)
|
||||
.padding(.horizontal, 26.7)
|
||||
|
||||
Button(action: { viewModel.login() }) {
|
||||
Text("로그인")
|
||||
.font(.custom(Font.bold.rawValue, size: 15))
|
||||
.frame(width: screenSize().width - 26.6, height: 46.7)
|
||||
.foregroundColor(.white)
|
||||
.background(Color(hex: "9970ff"))
|
||||
.cornerRadius(6.7)
|
||||
}
|
||||
.padding(.top, 40)
|
||||
|
||||
HStack(spacing: 10) {
|
||||
Text("비밀번호 재설정")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
.onTapGesture { AppState.shared.setAppStep(step: .findPassword) }
|
||||
|
||||
Text("|")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
|
||||
Text("회원가입")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
.onTapGesture { AppState.shared.setAppStep(step: .signUp) }
|
||||
}
|
||||
.padding(.top, 40)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
if viewModel.isLoading {
|
||||
LoadingView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LoginView()
|
||||
}
|
||||
}
|
74
SodaLive/Sources/User/Login/LoginViewModel.swift
Normal file
74
SodaLive/Sources/User/Login/LoginViewModel.swift
Normal file
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// LoginViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/09.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Moya
|
||||
import Combine
|
||||
|
||||
final class LoginViewModel: ObservableObject {
|
||||
private let repository = UserRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var email = ""
|
||||
@Published var password = ""
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
func login() {
|
||||
if email.isEmpty {
|
||||
self.errorMessage = "이메일을 입력해 주세요."
|
||||
self.isShowPopup = true
|
||||
return
|
||||
}
|
||||
|
||||
if password.isEmpty {
|
||||
self.errorMessage = "비밀번호를 입력해 주세요."
|
||||
self.isShowPopup = true
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
repository.login(request: LoginRequest(email: email, password: password))
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
}
|
||||
} receiveValue: { response in
|
||||
self.isLoading = false
|
||||
let responseData = response.data
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<LoginResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
UserDefaults.set(data.profileImage, forKey: .profileImage)
|
||||
UserDefaults.set(data.nickname, forKey: .nickname)
|
||||
UserDefaults.set(data.userId, forKey: .userId)
|
||||
UserDefaults.set(data.email, forKey: .email)
|
||||
UserDefaults.set(data.token, forKey: .token)
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
//
|
||||
// LoginView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/09.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LoginView: View {
|
||||
var body: some View {
|
||||
Text("Login View")
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LoginView()
|
||||
}
|
||||
}
|
57
SodaLive/Sources/User/UserApi.swift
Normal file
57
SodaLive/Sources/User/UserApi.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// UserApi.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/09.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Moya
|
||||
|
||||
enum UserApi {
|
||||
case login(request: LoginRequest)
|
||||
case signUp(parameters: [MultipartFormData])
|
||||
}
|
||||
|
||||
extension UserApi: TargetType {
|
||||
var baseURL: URL {
|
||||
return URL(string: BASE_URL)!
|
||||
}
|
||||
|
||||
var path: String {
|
||||
switch self {
|
||||
case .login:
|
||||
return "/member/login"
|
||||
|
||||
case .signUp:
|
||||
return "/member/signup"
|
||||
}
|
||||
}
|
||||
|
||||
var method: Moya.Method {
|
||||
switch self {
|
||||
case .login, .signUp:
|
||||
return .post
|
||||
}
|
||||
}
|
||||
|
||||
var task: Task {
|
||||
switch self {
|
||||
case .login(let request):
|
||||
return .requestJSONEncodable(request)
|
||||
|
||||
case .signUp(let parameters):
|
||||
return .uploadMultipart(parameters)
|
||||
}
|
||||
}
|
||||
|
||||
var headers: [String : String]? {
|
||||
switch self {
|
||||
case .login, .signUp:
|
||||
return nil
|
||||
|
||||
default:
|
||||
return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"]
|
||||
}
|
||||
}
|
||||
}
|
23
SodaLive/Sources/User/UserRepository.swift
Normal file
23
SodaLive/Sources/User/UserRepository.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// UserRepository.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/09.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CombineMoya
|
||||
import Combine
|
||||
import Moya
|
||||
|
||||
final class UserRepository {
|
||||
private let api = MoyaProvider<UserApi>()
|
||||
|
||||
func login(request: LoginRequest) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.login(request: request))
|
||||
}
|
||||
|
||||
func signUp(parameters: [MultipartFormData]) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.signUp(parameters: parameters))
|
||||
}
|
||||
}
|
84
SodaLive/Sources/User/UserTextField.swift
Normal file
84
SodaLive/Sources/User/UserTextField.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// UserTextField.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/09.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct UserTextField: View {
|
||||
|
||||
let title: String
|
||||
let hint: String
|
||||
let isSecure: Bool
|
||||
@Binding var variable: String
|
||||
|
||||
var isPasswordVisibleButton: Bool = false
|
||||
var keyboardType: UIKeyboardType = .default
|
||||
|
||||
@State private var visiblePassword = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(title)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.padding(.leading, 6.7)
|
||||
|
||||
if isSecure && !visiblePassword{
|
||||
SecureField(hint, text: $variable)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.padding(.top, 12)
|
||||
.padding(.leading, 6.7)
|
||||
} else {
|
||||
TextField(hint, text: $variable)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.keyboardType(keyboardType)
|
||||
.padding(.top, 12)
|
||||
.padding(.leading, 6.7)
|
||||
}
|
||||
|
||||
Divider()
|
||||
.frame(height: 0.3)
|
||||
.foregroundColor(Color(hex: "909090"))
|
||||
.padding(.top, 8.3)
|
||||
|
||||
if isSecure && isPasswordVisibleButton {
|
||||
Button(action: { visiblePassword.toggle() }) {
|
||||
HStack(spacing: 13.3) {
|
||||
if visiblePassword {
|
||||
Image("btn_select_checked")
|
||||
} else {
|
||||
Image("btn_select_normal")
|
||||
}
|
||||
|
||||
Text("비밀번호 표시")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
}
|
||||
}
|
||||
.padding(.top, 20)
|
||||
.padding(.leading, 6.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserTextField_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UserTextField(
|
||||
title: "이메일",
|
||||
hint: "user_id@email.com",
|
||||
isSecure: true,
|
||||
variable: .constant("test"),
|
||||
isPasswordVisibleButton: true
|
||||
)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user