로그인 페이지

This commit is contained in:
Yu Sung 2023-08-09 19:04:26 +09:00
parent c9c1b5f3c3
commit 1bc62f8fbd
28 changed files with 697 additions and 20 deletions

View File

@ -9,6 +9,15 @@
"version" : "1.2022062300.0" "version" : "1.2022062300.0"
} }
}, },
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git",
"state" : {
"revision" : "bc268c28fb170f494de9e9927c371b8342979ece",
"version" : "5.7.1"
}
},
{ {
"identity" : "firebase-ios-sdk", "identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@ -63,6 +72,15 @@
"version" : "3.1.1" "version" : "3.1.1"
} }
}, },
{
"identity" : "kingfisher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher.git",
"state" : {
"revision" : "c75584ac759cbb16b204d0a7de3ebf53ea6b304d",
"version" : "7.9.0"
}
},
{ {
"identity" : "leveldb", "identity" : "leveldb",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@ -72,6 +90,15 @@
"version" : "1.22.2" "version" : "1.22.2"
} }
}, },
{
"identity" : "moya",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Moya/Moya.git",
"state" : {
"revision" : "c263811c1f3dbf002be9bd83107f7cdc38992b26",
"version" : "15.0.3"
}
},
{ {
"identity" : "nanopb", "identity" : "nanopb",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@ -81,6 +108,15 @@
"version" : "2.30909.0" "version" : "2.30909.0"
} }
}, },
{
"identity" : "popupview",
"kind" : "remoteSourceControl",
"location" : "https://github.com/exyte/PopupView.git",
"state" : {
"revision" : "68349a0ae704b9a7041f756f3f4f460ddbf7ba8d",
"version" : "2.6.0"
}
},
{ {
"identity" : "promises", "identity" : "promises",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@ -90,6 +126,33 @@
"version" : "2.3.1" "version" : "2.3.1"
} }
}, },
{
"identity" : "reactiveswift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ReactiveCocoa/ReactiveSwift.git",
"state" : {
"revision" : "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c",
"version" : "6.7.0"
}
},
{
"identity" : "refreshablescrollview",
"kind" : "remoteSourceControl",
"location" : "https://github.com/phuhuynh2411/RefreshableScrollView",
"state" : {
"revision" : "e06edf5dc4facc7fbf71179e8a94f0d1c7035ce3",
"version" : "1.1.1"
}
},
{
"identity" : "rxswift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ReactiveX/RxSwift.git",
"state" : {
"revision" : "9dcaa4b333db437b0fbfaf453fad29069044a8b4",
"version" : "6.6.0"
}
},
{ {
"identity" : "swift-protobuf", "identity" : "swift-protobuf",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "btn_select_checked.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "btn_select_normal.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "loading_1.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "loading_2.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "loading_3.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "loading_4.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "loading_5.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -11,4 +11,8 @@ enum AppStep {
case splash case splash
case main case main
case signUp
case findPassword
} }

View 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?
}

View 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()
}
}

View 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: "") {}
}
}

View 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
}

View 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
}

View 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()
}
}

View 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)
}
}

View File

@ -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()
}
}

View 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))"]
}
}
}

View 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))
}
}

View 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
)
}
}