Skip to content

Commit

Permalink
Merge pull request #49 from School-of-Company/48-SignIn-FeatureMake
Browse files Browse the repository at this point in the history
🔀 :: [#48] 로그인 기능 구현
  • Loading branch information
Xixn2 authored Nov 10, 2024
2 parents 59fa9f4 + 277327e commit 78054ce
Show file tree
Hide file tree
Showing 8 changed files with 351 additions and 176 deletions.
3 changes: 1 addition & 2 deletions Projects/App/Sources/Application/ExpoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import SwiftUI
struct ExpoApp: App {
var body: some Scene {
WindowGroup {
SignupView(viewModel: AuthViewModel())
SigninView(viewModel: AuthViewModel())
}
}
}

65 changes: 55 additions & 10 deletions Projects/App/Sources/Extension/EXPOToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,45 @@ import Domain
import Security
import Foundation

// 토큰과 만료 기간을 포함하는 구조체
struct TokenData: Codable {
let token: String
let expirationDate: Date
}

public class KeyChain {
public static let shared = KeyChain()

// 토큰 저장하기 (만료 기간과 함께)
func create(key: String, token: String) {
let query: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key,
kSecValueData: token.data(using: .utf8, allowLossyConversion: false) as Any
]
SecItemDelete(query)
let status = SecItemAdd(query, nil)
assert(status == noErr, "failed to save Token")
// 먼저 토큰을 데이터로 변환하고 만료 기간을 설정
if let tokenData = token.data(using: .utf8) {
let query: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key,
kSecValueData: tokenData
]
SecItemDelete(query) // 기존 데이터 삭제
let status = SecItemAdd(query, nil)
assert(status == noErr, "failed to save Token")
}
}

// 토큰과 만료 기간을 JSON으로 저장하기
func saveTokenWithExpiration(key: String, token: String, expiresIn: TimeInterval) {
let expirationDate = Date().addingTimeInterval(expiresIn) // 만료일 계산
let tokenData = TokenData(token: token, expirationDate: expirationDate)

if let encodedData = try? JSONEncoder().encode(tokenData),
let tokenString = String(data: encodedData, encoding: .utf8) {
create(key: key, token: tokenString)
print("Token 저장 완료: \(tokenString)")
} else {
print("TokenData 인코딩 실패")
}
}

// 저장된 토큰 읽기 (만료 기간 포함)
public func read(key: String) -> String? {
let query: NSDictionary = [
kSecClass: kSecClassGenericPassword,
Expand All @@ -38,7 +63,7 @@ public class KeyChain {

if status == errSecSuccess {
if let retrievedData: Data = dataTypeRef as? Data {
let value = String(data: retrievedData, encoding: String.Encoding.utf8)
let value = String(data: retrievedData, encoding: .utf8)
return value
} else { return nil }
} else {
Expand All @@ -47,9 +72,27 @@ public class KeyChain {
}
}

// 토큰과 만료 기간을 함께 읽기
func loadTokenWithExpiration(key: String) -> TokenData? {
if let tokenString = read(key: key),
let tokenData = tokenString.data(using: .utf8) {
return try? JSONDecoder().decode(TokenData.self, from: tokenData)
}
return nil
}

// 만료 여부 확인
func isTokenExpired(key: String) -> Bool {
if let tokenData = loadTokenWithExpiration(key: key) {
return tokenData.expirationDate < Date() // 현재 시간과 만료일 비교
}
return true // 토큰이 없으면 만료된 것으로 간주
}

// 토큰 업데이트하기
func updateItem(token: Any, key: Any) -> Bool {
let prevQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key]
kSecAttrAccount: key]
let updateQuery: [CFString: Any] = [kSecValueData: (token as AnyObject).data(using: String.Encoding.utf8.rawValue) as Any]

let result: Bool = {
Expand All @@ -63,6 +106,7 @@ public class KeyChain {
return result
}

// 토큰 삭제
func delete(key: String) {
let query: NSDictionary = [
kSecClass: kSecClassGenericPassword,
Expand All @@ -83,3 +127,4 @@ public struct Const {
}
}


This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// PostDetailView.swift
// Expo-iOS
//
// Created by 서지완 on 11/6/24.
// Copyright © 2024 SchoolofCompany. All rights reserved.
//

import SwiftUI

struct PostDetailView: View {
var body: some View {
VStack(spacing: 0) {
ExpoIOSAsset.Assets.leftBackButton.swiftUIImage

Spacer()
}
}
}

#Preview {
PostDetailView()
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct SigninView: View {
@State private var showError: Bool = false
@State private var isActiveSignin = false
@State private var isActiveSignup = false
@StateObject var viewModel: AuthViewModel
@Environment(\.dismiss) var dismiss

var body: some View {
Expand Down Expand Up @@ -85,7 +86,17 @@ struct SigninView: View {
horizontalPadding: 160,
verticalPadding: 14
){
self.isActiveSignin.toggle()
viewModel.setupEmail(email: emailTextField)
viewModel.setupPassword(password: passwordTextField)
viewModel.signIn { serverCode, accessToken, refreshToken in
if (200..<300).contains(serverCode) {
print("로그인 성공(Success)")
isActiveSignin = true
} else {
print("로그인 실패(Fail)")
}
}

}
.navigationDestination(isPresented: $isActiveSignin) {
SignupView(viewModel: AuthViewModel())
Expand All @@ -97,7 +108,7 @@ struct SigninView: View {
}
}
}

#Preview {
SigninView()
}
//
//#Preview {
// SigninView(viewModel: AuthViewModel())
//}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// SigninViewModel.swift
// AuthViewModel.swift
// Expo-iOS
//
// Created by 서지완 on 11/4/24.
Expand All @@ -11,15 +11,10 @@ import Domain
import Foundation

public final class AuthViewModel: ObservableObject {

private let authProvider = MoyaProvider<AuthAPI>()
//private let accountProvider = MoyaProvider<AccountServices>()

//public override init() {}

var userData: SigninModel?
private var email: String = ""
//public let profileModel = ProfileViewModel()

private var password: String = ""
private var authCode: String = ""
Expand Down Expand Up @@ -85,6 +80,101 @@ public final class AuthViewModel: ObservableObject {
self.major = major
}

func signIn(completion: @escaping (Int, String?, String?) -> Void) {
let param = SigninRequest(nickname: email, password: password)
authProvider.request(.signIn(param: param)) { [weak self] response in
guard let self = self else { return }

switch response {
case .success(let result):
let statusCode = result.statusCode
do {
switch statusCode {
case 200:
let signInResponse = try result.map(SigninResponse.self)

// 만료 시간을 현재로부터의 시간 대신 `Date(timeIntervalSince1970:)`으로 변환
if let accessTokenExpiresIn = Double(signInResponse.accessTokenExpiresIn),
let refreshTokenExpiresIn = Double(signInResponse.refreshTokenExpiresIn) {

let accessTokenExpirationDate = Date(timeIntervalSince1970: accessTokenExpiresIn)
let refreshTokenExpirationDate = Date(timeIntervalSince1970: refreshTokenExpiresIn)

KeyChain.shared.saveTokenWithExpiration(
key: Const.KeyChainKey.accessToken,
token: signInResponse.accessToken,
expiresIn: accessTokenExpirationDate.timeIntervalSinceNow
)

KeyChain.shared.saveTokenWithExpiration(
key: Const.KeyChainKey.refreshToken,
token: signInResponse.refreshToken,
expiresIn: refreshTokenExpirationDate.timeIntervalSinceNow
)

// 만료 시간 확인을 위한 출력
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.timeZone = TimeZone.current

print("Access Token 만료 날짜: \(dateFormatter.string(from: accessTokenExpirationDate))")
print("Refresh Token 만료 날짜: \(dateFormatter.string(from: refreshTokenExpirationDate))")
} else {
print("만료 시간 변환 오류: accessTokenExpiresIn 또는 refreshTokenExpiresIn이 Double로 변환되지 않음.")
}

DispatchQueue.main.async {
completion(statusCode, signInResponse.accessToken, signInResponse.refreshToken)
}

case 400:
print("400ㅣ비밀번호가 일치하지 않습니다.")
DispatchQueue.main.async {
completion(statusCode, nil, nil)
}

case 403:
print("403ㅣ아직 보류중입니다.")
DispatchQueue.main.async {
completion(statusCode, nil, nil)
}

case 404:
print("404ㅣ계정을 찾지 못했습니다.")
DispatchQueue.main.async {
completion(statusCode, nil, nil)
}

case 500..<600:
print("\(statusCode)ㅣ서버오류입니다.")
DispatchQueue.main.async {
completion(statusCode, nil, nil)
}

default:
print("Unhandled status code: \(statusCode)")
DispatchQueue.main.async {
completion(statusCode, nil, nil)
}
}
} catch {
print("Error parsing SignInResponse: \(error)")
DispatchQueue.main.async {
completion(statusCode, nil, nil)
}
}

case .failure(let err):
print("Network error: \(err.localizedDescription)")
DispatchQueue.main.async {
completion(0, nil, nil)
}
}
}
}



// MARK: - Sign Up
func signUp(completion: @escaping (Bool) -> Void) {
let params = SignupRequest(name: name, nickname: nickname, email: email, password: password, phoneNumber: phoneNumber)
Expand All @@ -95,10 +185,14 @@ public final class AuthViewModel: ObservableObject {
let statusCode = result.statusCode
switch statusCode {
case 201:
print("Created")
print("201ㅣ회원가입이 완료되었습니다.")
completion(true)
case 404:
print("404ㅣSMS인증이 완료되지 않았습니다.")
case 409:
print("409ㅣ회원정보가 이미 존재합니다.")
case 500:
print("SERVER ERROR")
print("500ㅣ서버 에러입니다.")
completion(false)
default:
print("Unhandled status code: \(statusCode)")
Expand Down Expand Up @@ -173,12 +267,6 @@ public final class AuthViewModel: ObservableObject {
case 200..<300:
print("OK")
completion(true)
case 404:
print("인증 코드를 찾을 수 없을때 / 인증되지 않은 사용자일때 / 찾을 수 없는 사용자 일때")
completion(false)
case 429:
print("인증번호 검증 요청이 5번을 초과할 경우")
completion(false)
case 500:
print("SERVER ERROR")
completion(false)
Expand Down
Loading

0 comments on commit 78054ce

Please sign in to comment.