From 5370ef262a33c3e2a625f2d9fe1a6ac6f589163d Mon Sep 17 00:00:00 2001 From: SongCodeMaster Date: Thu, 8 May 2025 13:41:05 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20keychain=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=84=A0=EC=96=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Starbuck.xcodeproj/project.pbxproj | 3 + .../Sources/Utillity/KeychainWrapper.swift | 66 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 jengyoon/Starbuck/Starbuck/Sources/Utillity/KeychainWrapper.swift diff --git a/jengyoon/Starbuck/Starbuck.xcodeproj/project.pbxproj b/jengyoon/Starbuck/Starbuck.xcodeproj/project.pbxproj index 59b8dfd..2b881b7 100644 --- a/jengyoon/Starbuck/Starbuck.xcodeproj/project.pbxproj +++ b/jengyoon/Starbuck/Starbuck.xcodeproj/project.pbxproj @@ -88,6 +88,7 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + FF5FBBBB2DCC697500646675 /* Utillity */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Utillity; sourceTree = ""; }; FF6F74DB2D9DAC70003079EE /* Home */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Home; sourceTree = ""; }; FF6F75002D9E551F003079EE /* Home */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Home; sourceTree = ""; }; FF6F75012D9E5529003079EE /* Login */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Login; sourceTree = ""; }; @@ -159,6 +160,7 @@ 3A2543C67DA0A3DAC99340AA /* Sources */ = { isa = PBXGroup; children = ( + FF5FBBBB2DCC697500646675 /* Utillity */, FF89C1842DA78559002BEBA9 /* Navigation */, FF96846D2D932655009914B3 /* Components */, BB0F89AD98F7C1411A6B3723 /* Extentions */, @@ -328,6 +330,7 @@ dependencies = ( ); fileSystemSynchronizedGroups = ( + FF5FBBBB2DCC697500646675 /* Utillity */, FF6F74DB2D9DAC70003079EE /* Home */, FF6F75002D9E551F003079EE /* Home */, FF6F75012D9E5529003079EE /* Login */, diff --git a/jengyoon/Starbuck/Starbuck/Sources/Utillity/KeychainWrapper.swift b/jengyoon/Starbuck/Starbuck/Sources/Utillity/KeychainWrapper.swift new file mode 100644 index 0000000..90659c0 --- /dev/null +++ b/jengyoon/Starbuck/Starbuck/Sources/Utillity/KeychainWrapper.swift @@ -0,0 +1,66 @@ +// +// KeychainWrapper.swift +// Starbuck +// +// Created by 송승윤 on 5/8/25. +// + +import Foundation +import Security + +/// Keychain에 문자열 값을 저장, 불러오기, 삭제하는 유틸리티 +enum KeychainKey: String { + case email + case password + case nickname +} + +class KeychainWrapper { + + /// Keychain에 문자열 저장하기 + @discardableResult + static func save(_ value: String, for key: KeychainKey) -> Bool { + guard let data = value.data(using: .utf8) else { return false } + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: key.rawValue, + kSecValueData as String: data + ] + + // 기존 항목 삭제 후 새로 저장 (중복 방지) + SecItemDelete(query as CFDictionary) + return SecItemAdd(query as CFDictionary, nil) == errSecSuccess + } + + /// Keychain에서 문자열 불러오기 + static func load(for key: KeychainKey) -> String? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: key.rawValue, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne + ] + + var dataTypeRef: AnyObject? + let status = SecItemCopyMatching(query as NSDictionary, &dataTypeRef) + + // 값이 존재하면 문자열로 변환하여 반환 + if status == errSecSuccess, + let data = dataTypeRef as? Data, + let result = String(data: data, encoding: .utf8) { + return result + } + + return nil + } + + /// Keychain에서 항목 삭제 + @discardableResult + static func delete(for key: KeychainKey) -> Bool { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: key.rawValue + ] + return SecItemDelete(query as CFDictionary) == errSecSuccess + } +} From 67dbcf7e9e67dec02e2dc44b882e09f2fadf5e68 Mon Sep 17 00:00:00 2001 From: SongCodeMaster Date: Thu, 8 May 2025 13:46:42 +0900 Subject: [PATCH 2/6] =?UTF-8?q?refector:=20=EA=B8=B0=EC=A1=B4=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=A0=95=EB=B3=B4=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20AppStorage=20->=20KeyChain=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModels/Login/SignupViewModel.swift | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/SignupViewModel.swift b/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/SignupViewModel.swift index f10c6f6..0a8826d 100644 --- a/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/SignupViewModel.swift +++ b/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/SignupViewModel.swift @@ -5,31 +5,47 @@ // Created by 송승윤 on 3/27/25. /// 회원가입 화면에서 사용하는 ViewModel -/// 사용자 입력값을 AppStorage(UserDefaults)와 연동하여 저장 +/// 사용자 입력값을 Keychain에 저장 import Foundation import SwiftUI class SignupViewModel: ObservableObject { - @AppStorage("userEmail") private var userEmail: String = "" - @AppStorage("userPassword") private var userPassword: String = "" - @AppStorage("userNickname") private var userNickname: String = "" + // 사용자 입력값 @Published var email = "" @Published var password = "" @Published var nickname = "" + + // 회원가입 완료 여부 @Published var isSignupComplete = false + /// 입력 필드 검증 로직 var isFormValid: Bool { !email.isEmpty && !password.isEmpty && !nickname.isEmpty } + /// 회원가입 처리 함수 + /// 입력값을 Keychain에 저장한다. func signup() { - // AppStorage에 사용자 정보 저장 - userEmail = email - userPassword = password - userNickname = nickname + KeychainWrapper.save(email, for: .email) + KeychainWrapper.save(password, for: .password) + KeychainWrapper.save(nickname, for: .nickname) // 회원가입 완료 처리 isSignupComplete = true } -} + + /// Keycahin에서 기존 사용자 데이터를 불러와 필드에 반영하기 + func loadSavedUserData() { + email = KeychainWrapper.load(for: .email) ?? "" + password = KeychainWrapper.load(for: .password) ?? "" + nickname = KeychainWrapper.load(for: .nickname) ?? "" + } + + /// Keychain에 저장된 사용자 정보 삭제 + func clearUserData() { + KeychainWrapper.delete(for: .email) + KeychainWrapper.delete(for: .password) + KeychainWrapper.delete(for: .nickname) + } +} From 1626139301d0fdd3fe31b17931e05cd8fdef6c6a Mon Sep 17 00:00:00 2001 From: SongCodeMaster Date: Thu, 8 May 2025 14:12:45 +0900 Subject: [PATCH 3/6] =?UTF-8?q?refector:=20AppStorage=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20->=20Keychai?= =?UTF-8?q?n=EC=97=90=EC=84=9C=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModels/Home/HomeViewModel.swift | 7 +- .../ViewModels/Login/LoginViewModel.swift | 11 +- .../Sources/Views/Home/HomeView.swift | 1 - .../Sources/Views/Other/OtherView.swift | 102 +++++++++--------- 4 files changed, 62 insertions(+), 59 deletions(-) diff --git a/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Home/HomeViewModel.swift b/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Home/HomeViewModel.swift index db59d69..002730d 100644 --- a/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Home/HomeViewModel.swift +++ b/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Home/HomeViewModel.swift @@ -9,12 +9,11 @@ import Foundation import SwiftUI class HomeViewModel: ObservableObject { - /// 회원가입시 저장된 닉네임 불러오기 - @AppStorage("userNickname") private var nickname: String = "" - + /// 뷰에서 접근하는 닉네임 var displayName: String { - nickname.isEmpty ? "(설정 닉네임)" : nickname + let nickname = KeychainWrapper.load(for: .nickname) ?? "" + return nickname.isEmpty ? "(설정 닉네임)" : nickname } /// 추천 메뉴 더미 데이터 diff --git a/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/LoginViewModel.swift b/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/LoginViewModel.swift index 38190a9..6716b88 100644 --- a/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/LoginViewModel.swift +++ b/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/LoginViewModel.swift @@ -16,16 +16,19 @@ class LoginViewModel: ObservableObject { @Published var inputEmail: String = "" @Published var inputPassword: String = "" - // MARK: - AppStorage에 저장된 회원 정보 불러오기 - @AppStorage("userEmail") private var savedEmail: String = "" - @AppStorage("userPassword") private var savedPassword: String = "" - // MARK: - 로그인 상태 @Published var isLogin: Bool = false @Published var loginError: String? = nil // MARK: - 로그인 로직 func login() { + guard let savedEmail = KeychainWrapper.load(for: .email), + let savedPassword = KeychainWrapper.load(for: .password) else { + loginError = "저장된 사용자 정보가 없습니다." + isLogin = false + return + } + if (inputEmail == savedEmail && inputPassword == savedPassword) { isLogin = true loginError = nil diff --git a/jengyoon/Starbuck/Starbuck/Sources/Views/Home/HomeView.swift b/jengyoon/Starbuck/Starbuck/Sources/Views/Home/HomeView.swift index bd78ed3..c6eeb78 100644 --- a/jengyoon/Starbuck/Starbuck/Sources/Views/Home/HomeView.swift +++ b/jengyoon/Starbuck/Starbuck/Sources/Views/Home/HomeView.swift @@ -9,7 +9,6 @@ import SwiftUI struct HomeView: View { /// 관찰 가능한 객체를 HomeView에서 직접 생성후 소유한다. - /// AppStorage에 저장된 닉네임과 더미데이터 랜더링 @StateObject private var viewModel = HomeViewModel() @State private var showAdvertisement = false diff --git a/jengyoon/Starbuck/Starbuck/Sources/Views/Other/OtherView.swift b/jengyoon/Starbuck/Starbuck/Sources/Views/Other/OtherView.swift index 662b2f6..f6ead01 100644 --- a/jengyoon/Starbuck/Starbuck/Sources/Views/Other/OtherView.swift +++ b/jengyoon/Starbuck/Starbuck/Sources/Views/Other/OtherView.swift @@ -9,8 +9,9 @@ import SwiftUI struct OtherView: View { /// 회원가입시 저장한 닉네임을 표시 - /// UserDefaults의 "nickname" 키에 저장된 값을 불러온다. - @AppStorage("nickname") private var nickname : String? + /// 키체인에서 닉네임 불러온 닉네임 상태 변수 + @State private var nickname: String = "" + @Environment(\.dismiss) private var dismiss @EnvironmentObject private var router: NavigationRouter @@ -31,8 +32,10 @@ struct OtherView: View { otherBottomView }//: VStack } //: ZStack - - + .onAppear { + // 뷰 등장 시 닉네임 로드 + nickname = KeychainWrapper.load(for: .nickname) ?? "" + } } // MARK: - Properties @@ -49,8 +52,8 @@ struct OtherView: View { dismiss() }) { Image("logout") - .resizable() - .frame(width: 35, height: 35) + .resizable() + .frame(width: 35, height: 35) } } .padding(.horizontal, 20) @@ -61,15 +64,14 @@ struct OtherView: View { private var otherTitleView: some View { VStack (spacing: 24) { Group { - if let nickname { - Text("\(nickname)") - .foregroundStyle(Color(.green01)) - + Text("님") - } - else { + if nickname.isEmpty { Text("작성한 닉네임") - .foregroundStyle(Color(.green01)) - + Text("님") + .foregroundStyle(Color(.green01)) + + Text("님") + } else { + Text(nickname) + .foregroundStyle(Color(.green01)) + + Text("님") } Text("환영합니다! 🙌") } @@ -86,42 +88,42 @@ struct OtherView: View { /// 결제 관련 버튼 뷰 /// - 버튼을 컴포넌트화 하여 재사용성 높임 - private var otherPayView: some View { - VStack() { - HStack { - Text("Pay") - .font(.PretendardSemiBold18) - .frame(height: 28) - - Spacer() - } - - Spacer().frame(height: 8) - - HStack { - OtherViewButton(text: "스타벅스 카드 등록", textColor: .black, font: .PretendardSemiBold16, icon: "other2.1", action: {print("스타벅스 카드 등록 클릭")}) - - Spacer() - - OtherViewButton(text: "카드 교환권 등록", textColor: .black, font: .PretendardSemiBold16, icon: "other2.2", action: {print("카드 교환권 클릭")}) - - Spacer().frame(width: 10) - } - .padding(.vertical, 16) - - HStack { - OtherViewButton(text: "쿠폰 등록", textColor: .black, font: .PretendardSemiBold16, icon: "other2.3", action: {print("쿠폰 등록 클릭")}) - - Spacer() - - OtherViewButton(text: "쿠폰 히스토리", textColor: .black ,font: .PretendardSemiBold16, icon: "other2.4", action: {print("쿠폰 히스토리 클릭")}) - - Spacer().frame(width: 30) - } - .padding(.vertical, 16) - } //: VStack - .padding(.horizontal, 10) - } + private var otherPayView: some View { + VStack() { + HStack { + Text("Pay") + .font(.PretendardSemiBold18) + .frame(height: 28) + + Spacer() + } + + Spacer().frame(height: 8) + + HStack { + OtherViewButton(text: "스타벅스 카드 등록", textColor: .black, font: .PretendardSemiBold16, icon: "other2.1", action: {print("스타벅스 카드 등록 클릭")}) + + Spacer() + + OtherViewButton(text: "카드 교환권 등록", textColor: .black, font: .PretendardSemiBold16, icon: "other2.2", action: {print("카드 교환권 클릭")}) + + Spacer().frame(width: 10) + } + .padding(.vertical, 16) + + HStack { + OtherViewButton(text: "쿠폰 등록", textColor: .black, font: .PretendardSemiBold16, icon: "other2.3", action: {print("쿠폰 등록 클릭")}) + + Spacer() + + OtherViewButton(text: "쿠폰 히스토리", textColor: .black ,font: .PretendardSemiBold16, icon: "other2.4", action: {print("쿠폰 히스토리 클릭")}) + + Spacer().frame(width: 30) + } + .padding(.vertical, 16) + } //: VStack + .padding(.horizontal, 10) + } /// 고객지원 뷰 /// - 버튼을 컴포넌트화 하여 재사용성 높임 From 5f3fc32605913f2e1be0c175339ab08dc515fdf9 Mon Sep 17 00:00:00 2001 From: SongCodeMaster Date: Thu, 8 May 2025 17:40:11 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=EC=9D=B8=EA=B0=80=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=84=EB=8B=AC=20=EB=B0=9B=EA=B8=B0=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20AppDelegate=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Starbuck.xcodeproj/project.pbxproj | 4 +++ .../Starbuck/Sources/AppDelegate.swift | 32 +++++++++++++++++++ .../Starbuck/Sources/StarbuckApp.swift | 2 ++ .../ViewModels/Login/LoginViewModel.swift | 4 +++ 4 files changed, 42 insertions(+) create mode 100644 jengyoon/Starbuck/Starbuck/Sources/AppDelegate.swift diff --git a/jengyoon/Starbuck/Starbuck.xcodeproj/project.pbxproj b/jengyoon/Starbuck/Starbuck.xcodeproj/project.pbxproj index 2b881b7..ac57550 100644 --- a/jengyoon/Starbuck/Starbuck.xcodeproj/project.pbxproj +++ b/jengyoon/Starbuck/Starbuck.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ C87EE176AD207D08A51D8E1C /* Pretendard-Thin.otf in Resources */ = {isa = PBXBuildFile; fileRef = 6F9415C82BD901C6AA812805 /* Pretendard-Thin.otf */; }; D137A0CE8238989E873328EF /* Pretendard-ExtraBold.otf in Resources */ = {isa = PBXBuildFile; fileRef = D2E6CFDB2FE55DB301BB7E93 /* Pretendard-ExtraBold.otf */; }; F9B59708E65FFDC479FA292D /* Pretendard-SemiBold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 1C05EE3A01621AA71D5A3474 /* Pretendard-SemiBold.otf */; }; + FF5FBBBF2DCCA35000646675 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5FBBBE2DCCA35000646675 /* AppDelegate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -85,6 +86,7 @@ F8D54BB8F383F5345CEFC4FB /* Pretendard-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Bold.otf"; sourceTree = ""; }; F971E5B6815CB1BE429AAFDA /* CustomColor.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = CustomColor.xcassets; sourceTree = ""; }; FB5860E6E14B3D180E9A7ABF /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + FF5FBBBE2DCCA35000646675 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -169,6 +171,7 @@ D63F2DD2A4425E7FF0092A12 /* ViewModels */, 3CFD97B39B9B1F9402805C71 /* Views */, CF9B6D9BD6286FD79B02F2D7 /* StarbuckApp.swift */, + FF5FBBBE2DCCA35000646675 /* AppDelegate.swift */, ); path = Sources; sourceTree = ""; @@ -451,6 +454,7 @@ 9F4761109412ED75AEFC6F93 /* ColorExtension.swift in Sources */, 7840DC737DE4A9321BFA6B04 /* FontManager.swift in Sources */, B2EECE92459A9FE846F262F6 /* StarbuckApp.swift in Sources */, + FF5FBBBF2DCCA35000646675 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/jengyoon/Starbuck/Starbuck/Sources/AppDelegate.swift b/jengyoon/Starbuck/Starbuck/Sources/AppDelegate.swift new file mode 100644 index 0000000..5544437 --- /dev/null +++ b/jengyoon/Starbuck/Starbuck/Sources/AppDelegate.swift @@ -0,0 +1,32 @@ +// +// AppDelegate.swift +// Starbuck +// +// Created by 송승윤 on 5/8/25. +// + +import UIKit + +final class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ app: UIApplication, open url: URL, + options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { + if url.scheme == "myapp", url.host == "oauth" { + if let code = URLComponents(string: url.absoluteString)? + .queryItems?.first(where: { $0.name == "code" })?.value { + + // 인가 코드 Notification으로 전달 + NotificationCenter.default.post( + name: .didReceiveKakaoCode, + object: nil, + userInfo: ["code": code] + ) + } + return true + } + return false + } +} + +extension Notification.Name { + static let didReceiveKakaoCode = Notification.Name("didReceieveKakaoCode") +} diff --git a/jengyoon/Starbuck/Starbuck/Sources/StarbuckApp.swift b/jengyoon/Starbuck/Starbuck/Sources/StarbuckApp.swift index ba3993c..5cc808b 100644 --- a/jengyoon/Starbuck/Starbuck/Sources/StarbuckApp.swift +++ b/jengyoon/Starbuck/Starbuck/Sources/StarbuckApp.swift @@ -2,6 +2,8 @@ import SwiftUI @main struct StarbuckApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + var body: some Scene { WindowGroup { AppRootView() diff --git a/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/LoginViewModel.swift b/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/LoginViewModel.swift index 6716b88..0306e2f 100644 --- a/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/LoginViewModel.swift +++ b/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/LoginViewModel.swift @@ -41,4 +41,8 @@ class LoginViewModel: ObservableObject { var buttonValid: Bool { !inputEmail.isEmpty && !inputPassword.isEmpty } + + /// 카카오 로그인 성공 시 호출되는 메서드 + /// 토큰 요청 및.사용자 정보 요청은 kakaoLovinViewModel에서 처리 + } From d4b89cbdf43661465a44ec06aac081ad2876edd6 Mon Sep 17 00:00:00 2001 From: SongCodeMaster Date: Thu, 8 May 2025 18:27:27 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=EC=9D=B8=EA=B0=80=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EA=B0=90=EC=A7=80=ED=9B=84=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Model/Login/KakaoToken.swift | 24 ++++++ .../Login/KakaoLoginViewModel.swift | 82 +++++++++++++++++++ .../ViewModels/Login/LoginViewModel.swift | 6 +- .../Sources/Views/Login/LoginView.swift | 7 +- 4 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 jengyoon/Starbuck/Starbuck/Sources/Model/Login/KakaoToken.swift create mode 100644 jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/KakaoLoginViewModel.swift diff --git a/jengyoon/Starbuck/Starbuck/Sources/Model/Login/KakaoToken.swift b/jengyoon/Starbuck/Starbuck/Sources/Model/Login/KakaoToken.swift new file mode 100644 index 0000000..1ac82fd --- /dev/null +++ b/jengyoon/Starbuck/Starbuck/Sources/Model/Login/KakaoToken.swift @@ -0,0 +1,24 @@ +// +// KakaoToken.swift +// Starbuck +// +// Created by 송승윤 on 5/8/25. +// + +import Foundation + +struct KakaoToken: Decodable { + let access_token: String +} + +struct KakaoUser: Decodable { + let kakao_account: KakaoAccount +} + +struct KakaoAccount: Decodable { + let profile: KakaoProfile +} + +struct KakaoProfile: Decodable { + let nickname: String +} diff --git a/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/KakaoLoginViewModel.swift b/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/KakaoLoginViewModel.swift new file mode 100644 index 0000000..ab57e96 --- /dev/null +++ b/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/KakaoLoginViewModel.swift @@ -0,0 +1,82 @@ +// +// KakaoLoginViewModel.swift +// Starbuck +// +// Created by 송승윤 on 5/8/25. +// + +import Foundation +import UIKit +import Combine + +class KakaoLoginViewModel: ObservableObject { + var loginViewModel: LoginViewModel? // Login 상태를 갱신하기 위한 참조 + private var cancellables = Set() + + // 초기화: 카카오 로그인 과정에서 받은 authorization code를 처리하기 위한 NotificationCenter 구독 설정 + init() { + NotificationCenter.default.publisher(for: .didReceiveKakaoCode) + .compactMap { $0.userInfo?["code"] as? String } + .sink { [weak self] code in + self?.requestToken(with: code) // 받은 코드로 토큰 요청 시작 + } + .store(in: &cancellables) + } + + // 카카오 로그인 페이지로 이동하는 메서드 + func loginWithKakao() { + let clientID = "카카오 REST API 키" + let redirectURI = "myapp://oauth" + let urlStr = "https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=\(clientID)&redirect_uri=\(redirectURI)" + + // URL이 유효하면 카카오 로그인 페이지를 열어 인증 진행 + if let url = URL(string: urlStr) { + UIApplication.shared.open(url) + } + } + + // authorization code를 이용해 asccess token을 요청 + private func requestToken(with code: String) { + let url = URL(string: "https://kauth.kakao.com/oauth/token")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + + // 요청 파라미터 설정 + let params = [ + "grant_type": "authorization_code", + "client_id": "카카오 REST API 키", + "redirect_uri": "myapp://oauth", + "code": code + ] + request.httpBody = params.map { "\($0.key)=\($0.value)" } + .joined(separator: "&") + .data(using: .utf8) + + // 토큰 요청을 비동기로 수행 + URLSession.shared.dataTask(with: request) { data, _, _ in + guard let data = data, + let token = try? JSONDecoder().decode(KakaoToken.self, from: data) else { return } + // 토큰을 성공적으로 받으면 사용자 정보 요청 시작 + self.requestUserInfo(with: token.access_token) + }.resume() + } + + private func requestUserInfo(with accessToken: String) { + var request = URLRequest(url: URL(string: "https://kapi.kakao.com/v2/user/me")!) + request.httpMethod = "GET" + request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") + + // 사용자 정보 요청을 비동기로 수행 + URLSession.shared.dataTask(with: request) { data, _, _ in + guard let data = data, + let user = try? JSONDecoder().decode(KakaoUser.self, from: data) else { return } + + // 메인 스레드에서 로그인 상태를 갱신 + DispatchQueue.main.async { + let nickname = user.kakao_account.profile.nickname + self.loginViewModel?.loginWithKakao(nickname: nickname) + } + }.resume() + } +} diff --git a/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/LoginViewModel.swift b/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/LoginViewModel.swift index 0306e2f..2b76e4c 100644 --- a/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/LoginViewModel.swift +++ b/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/LoginViewModel.swift @@ -44,5 +44,9 @@ class LoginViewModel: ObservableObject { /// 카카오 로그인 성공 시 호출되는 메서드 /// 토큰 요청 및.사용자 정보 요청은 kakaoLovinViewModel에서 처리 - + func loginWithKakao(nickname: String) { + isLogin = true + loginError = nil + KeychainWrapper.save(nickname, for: .nickname) + } } diff --git a/jengyoon/Starbuck/Starbuck/Sources/Views/Login/LoginView.swift b/jengyoon/Starbuck/Starbuck/Sources/Views/Login/LoginView.swift index a0e4d3c..a60f9da 100644 --- a/jengyoon/Starbuck/Starbuck/Sources/Views/Login/LoginView.swift +++ b/jengyoon/Starbuck/Starbuck/Sources/Views/Login/LoginView.swift @@ -19,6 +19,7 @@ struct LoginView: View { @EnvironmentObject private var router: NavigationRouter @FocusState private var focusField: Field? @StateObject private var viewModel = LoginViewModel() + @StateObject private var kakaoVM = KakaoLoginViewModel() var body: some View { ZStack { @@ -120,7 +121,11 @@ struct LoginView: View { router.navigate(to: .signup) } - SocialLoginButton(buttonColor: Color.yellow, textColor: Color.black, text: "카카오 로그인", font: .PretendardMedium16, icon: "kakao", action: {}) + SocialLoginButton(buttonColor: Color.yellow, textColor: Color.black, text: "카카오 로그인", font: .PretendardMedium16, icon: "kakao", action: { + // 카카오 버튼을 누르면 LoginViewModel과 연결하여 로그인 절차 진행 + kakaoVM.loginViewModel = viewModel + kakaoVM.loginWithKakao() + }) SocialLoginButton(buttonColor: Color.black, textColor: Color.white, text: "Apple로 로그인", font: .PretendardMedium16, icon: "apple", action: {}) From fbfe21cb9917dd59b09129e976f7cb6236fc556d Mon Sep 17 00:00:00 2001 From: SongCodeMaster Date: Fri, 9 May 2025 01:38:39 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20APIKey=20=EA=B8=B0=EC=9E=85?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/ViewModels/Login/KakaoLoginViewModel.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/KakaoLoginViewModel.swift b/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/KakaoLoginViewModel.swift index ab57e96..b5d7ad7 100644 --- a/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/KakaoLoginViewModel.swift +++ b/jengyoon/Starbuck/Starbuck/Sources/ViewModels/Login/KakaoLoginViewModel.swift @@ -25,8 +25,8 @@ class KakaoLoginViewModel: ObservableObject { // 카카오 로그인 페이지로 이동하는 메서드 func loginWithKakao() { - let clientID = "카카오 REST API 키" - let redirectURI = "myapp://oauth" + let clientID = "" + let redirectURI = "https://songtarbuck.com/oauth" let urlStr = "https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=\(clientID)&redirect_uri=\(redirectURI)" // URL이 유효하면 카카오 로그인 페이지를 열어 인증 진행 @@ -37,7 +37,7 @@ class KakaoLoginViewModel: ObservableObject { // authorization code를 이용해 asccess token을 요청 private func requestToken(with code: String) { - let url = URL(string: "https://kauth.kakao.com/oauth/token")! + let url = URL(string: "")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") @@ -45,8 +45,8 @@ class KakaoLoginViewModel: ObservableObject { // 요청 파라미터 설정 let params = [ "grant_type": "authorization_code", - "client_id": "카카오 REST API 키", - "redirect_uri": "myapp://oauth", + "client_id": "", + "redirect_uri": "https://songtarbuck.com/oauth", "code": code ] request.httpBody = params.map { "\($0.key)=\($0.value)" }