From 42e375ec4bc1826ea791c2e34e7898e96d4b6930 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Wed, 28 Jan 2026 19:05:42 +0900 Subject: [PATCH] =?UTF-8?q?LINE=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LINE 로그인 요청과 토큰 처리 흐름을 추가함 --- .../xcshareddata/swiftpm/Package.resolved | 11 ++- SodaLive/Sources/App/AppDelegate.swift | 3 + SodaLive/Sources/App/SodaLiveApp.swift | 2 + SodaLive/Sources/Debug/Utils/Constants.swift | 2 + SodaLive/Sources/I18n/I18n.swift | 26 +++++++ SodaLive/Sources/User/Login/LoginView.swift | 1 + .../Sources/User/Login/LoginViewModel.swift | 72 +++++++++++++++++++ SodaLive/Sources/User/UserApi.swift | 11 ++- SodaLive/Sources/User/UserRepository.swift | 4 ++ SodaLive/Sources/Utils/Constants.swift | 2 + 10 files changed, 131 insertions(+), 3 deletions(-) diff --git a/SodaLive.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SodaLive.xcworkspace/xcshareddata/swiftpm/Package.resolved index f8297a9..c51020f 100644 --- a/SodaLive.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SodaLive.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "23fbb8fc47a95510f46840139efb1770d536256f32ac2ba9f74cf03ee90a3979", + "originHash" : "9f35428c4c178ca4a8bfa4b72544585a9e4d5b119825b423e1d2166cbe03fe37", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -154,6 +154,15 @@ "version" : "1.22.5" } }, + { + "identity" : "line-sdk-ios-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/line/line-sdk-ios-swift.git", + "state" : { + "revision" : "51ef2ebefb05db8f748e80208b3281ca723abcdb", + "version" : "5.14.0" + } + }, { "identity" : "moya", "kind" : "remoteSourceControl", diff --git a/SodaLive/Sources/App/AppDelegate.swift b/SodaLive/Sources/App/AppDelegate.swift index a975c44..11874fd 100644 --- a/SodaLive/Sources/App/AppDelegate.swift +++ b/SodaLive/Sources/App/AppDelegate.swift @@ -14,6 +14,7 @@ import FBSDKCoreKit import FirebaseCore import FirebaseAnalytics import FirebaseMessaging +import LineSDK class AppDelegate: UIResponder, UIApplicationDelegate { @@ -24,6 +25,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { FirebaseApp.configure() + LoginManager.shared.setup(channelID: LINE_CHANNEL_ID, universalLinkURL: nil) Notifly.initialize(projectId: NOTIFLY_PROJECT_ID, username: NOTIFLY_USERNAME, password: NOTIFLY_PASSWORD) Messaging.messaging().delegate = self setupAppsFlyer() @@ -75,6 +77,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void) -> Bool { + _ = LoginManager.shared.application(application, open: userActivity.webpageURL) AppsFlyerLib.shared().continue(userActivity, restorationHandler: nil) return true } diff --git a/SodaLive/Sources/App/SodaLiveApp.swift b/SodaLive/Sources/App/SodaLiveApp.swift index ac80a6f..295fc06 100644 --- a/SodaLive/Sources/App/SodaLiveApp.swift +++ b/SodaLive/Sources/App/SodaLiveApp.swift @@ -13,6 +13,7 @@ import AppsFlyerLib import GoogleSignIn import KakaoSDKCommon import KakaoSDKAuth +import LineSDK @main struct SodaLiveApp: App { @@ -83,6 +84,7 @@ struct SodaLiveApp: App { comps.path.lowercased() == "/result" { canPgPaymentViewModel.handleVerifyOpenURL(url) } else { + _ = LoginManager.shared.application(UIApplication.shared, open: url, options: [:]) ApplicationDelegate.shared.application(UIApplication.shared, open: url, options: [:]) AppsFlyerLib.shared().handleOpen(url) GIDSignIn.sharedInstance.handle(url) diff --git a/SodaLive/Sources/Debug/Utils/Constants.swift b/SodaLive/Sources/Debug/Utils/Constants.swift index 6b2db86..551784e 100644 --- a/SodaLive/Sources/Debug/Utils/Constants.swift +++ b/SodaLive/Sources/Debug/Utils/Constants.swift @@ -27,3 +27,5 @@ let GID_CLIENT_ID = "758414412471-3cf403jb4s405eu17qrfrcbs9ofhq369.apps.googleus let GID_SERVER_CLIENT_ID = "758414412471-mosodbj2chno7l1j0iihldh6edmk0gk9.apps.googleusercontent.com" let KAKAO_APP_KEY = "20cf19413d63bfdfd30e8e6dff933d33" + +let LINE_CHANNEL_ID = "2008995582" diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index 3e586a2..b90954f 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -1153,6 +1153,32 @@ enum I18n { ) } } + + enum Line { + static var openFailed: String { + pick( + ko: "라인 로그인 화면을 열 수 없습니다.\n다시 시도해 주세요.", + en: "Unable to open LINE sign-in.\nPlease try again.", + ja: "LINEログイン画面を開けません。\nもう一度お試しください。" + ) + } + + static var signInFailed: String { + pick( + ko: "라인 로그인에 실패했습니다.\n다시 시도해 주세요.", + en: "LINE sign-in failed.\nPlease try again.", + ja: "LINEログインに失敗しました。\nもう一度お試しください。" + ) + } + + static var tokenMissing: String { + pick( + ko: "라인 인증 토큰을 가져오지 못했습니다.", + en: "Failed to retrieve LINE token.", + ja: "LINE認証トークンを取得できませんでした。" + ) + } + } } // 문자 메시지(Text Message) 관련 문자열 diff --git a/SodaLive/Sources/User/Login/LoginView.swift b/SodaLive/Sources/User/Login/LoginView.swift index 1eb6b14..3251c95 100644 --- a/SodaLive/Sources/User/Login/LoginView.swift +++ b/SodaLive/Sources/User/Login/LoginView.swift @@ -150,6 +150,7 @@ struct LoginView: View { Image("ic_login_line") .onTapGesture { hideKeyboard() + viewModel.loginWithLine() } Image("ic_login_x") diff --git a/SodaLive/Sources/User/Login/LoginViewModel.swift b/SodaLive/Sources/User/Login/LoginViewModel.swift index aba09b5..cfc9c04 100644 --- a/SodaLive/Sources/User/Login/LoginViewModel.swift +++ b/SodaLive/Sources/User/Login/LoginViewModel.swift @@ -15,12 +15,14 @@ import UIKit import GoogleSignIn import KakaoSDKUser import KakaoSDKAuth +import LineSDK final class LoginViewModel: NSObject, ObservableObject { private let appViewModel = AppViewModel() private let repository = UserRepository() private var subscription = Set() private var currentNonce: String? + private var currentLineNonce: String? @Published var email = "" @Published var password = "" @@ -127,6 +129,49 @@ final class LoginViewModel: NSObject, ObservableObject { KakaoSDKUser.UserApi.shared.loginWithKakaoAccount(completion: loginHandler) } } + + func loginWithLine() { + guard let presentingViewController = presentingViewController() else { + self.errorMessage = I18n.Login.Line.openFailed + self.isShowPopup = true + return + } + + let nonce = randomNonceString() + currentLineNonce = nonce + + var parameters = LoginManager.Parameters() + parameters.IDTokenNonce = nonce + + DispatchQueue.main.async { + LoginManager.shared.login(permissions: [.profile, .openID], in: presentingViewController, parameters: parameters) { result in + switch result { + case .success(let loginResult): + guard let identityToken = loginResult.accessToken.IDTokenRaw, !identityToken.isEmpty else { + self.currentLineNonce = nil + self.errorMessage = I18n.Login.Line.tokenMissing + self.isShowPopup = true + return + } + + guard let lineNonce = self.currentLineNonce else { + self.errorMessage = I18n.Common.commonError + self.isShowPopup = true + return + } + + self.currentLineNonce = nil + self.loginWithLine(identityToken: identityToken, nonce: lineNonce) + + case .failure(let error): + self.currentLineNonce = nil + ERROR_LOG(error.localizedDescription) + self.errorMessage = I18n.Login.Line.signInFailed + self.isShowPopup = true + } + } + } + } private func loginWithApple(identityToken: String, nonce: String) { let pushToken = UserDefaults.string(forKey: .pushToken) @@ -209,6 +254,33 @@ final class LoginViewModel: NSObject, ObservableObject { .store(in: &subscription) } + private func loginWithLine(identityToken: String, nonce: String) { + let pushToken = UserDefaults.string(forKey: .pushToken) + let marketingPid = UserDefaults.string(forKey: .marketingPid) + let request = SocialLoginRequest( + container: "ios", + pushToken: pushToken.isEmpty ? nil : pushToken, + marketingPid: marketingPid.isEmpty ? nil : marketingPid, + identityToken: identityToken, + nonce: nonce + ) + + isLoading = true + repository.loginLine(request: request) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + self.isLoading = false + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { response in + self.handleLoginResponse(response) + } + .store(in: &subscription) + } + private func presentingViewController() -> UIViewController? { return UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } diff --git a/SodaLive/Sources/User/UserApi.swift b/SodaLive/Sources/User/UserApi.swift index 96577c9..eb41365 100644 --- a/SodaLive/Sources/User/UserApi.swift +++ b/SodaLive/Sources/User/UserApi.swift @@ -13,6 +13,7 @@ enum UserApi { case loginApple(request: SocialLoginRequest) case loginGoogle(request: SocialLoginRequest, idToken: String) case loginKakao(request: SocialLoginRequest, accessToken: String) + case loginLine(request: SocialLoginRequest) case signUp(request: SignUpRequest) case findPassword(request: ForgotPasswordRequest) case searchUser(nickname: String) @@ -58,6 +59,9 @@ extension UserApi: TargetType { case .loginKakao: return "/member/login/kakao" + + case .loginLine: + return "/member/login/line" case .signUp: return "/member/signup/v2" @@ -132,7 +136,7 @@ extension UserApi: TargetType { var method: Moya.Method { switch self { - case .login, .loginApple, .loginGoogle, .loginKakao, .signUp, .findPassword, .notification, .logout, .logoutAllDevice, .signOut, .creatorFollow, .creatorUnFollow, .memberBlock, .memberUnBlock, + case .login, .loginApple, .loginGoogle, .loginKakao, .loginLine, .signUp, .findPassword, .notification, .logout, .logoutAllDevice, .signOut, .creatorFollow, .creatorUnFollow, .memberBlock, .memberUnBlock, .profileImageUpdate: return .post @@ -157,6 +161,9 @@ extension UserApi: TargetType { case .loginKakao(let request, _): return .requestJSONEncodable(request) + + case .loginLine(let request): + return .requestJSONEncodable(request) case .signUp(let request): return .requestJSONEncodable(request) @@ -220,7 +227,7 @@ extension UserApi: TargetType { var headers: [String : String]? { switch self { - case .login, .loginApple, .signUp, .findPassword: + case .login, .loginApple, .loginLine, .signUp, .findPassword: return nil case .loginGoogle(_, let idToken): diff --git a/SodaLive/Sources/User/UserRepository.swift b/SodaLive/Sources/User/UserRepository.swift index 15c14aa..c7c584f 100644 --- a/SodaLive/Sources/User/UserRepository.swift +++ b/SodaLive/Sources/User/UserRepository.swift @@ -28,6 +28,10 @@ final class UserRepository { func loginKakao(request: SocialLoginRequest, accessToken: String) -> AnyPublisher { return api.requestPublisher(.loginKakao(request: request, accessToken: accessToken)) } + + func loginLine(request: SocialLoginRequest) -> AnyPublisher { + return api.requestPublisher(.loginLine(request: request)) + } func signUp(request: SignUpRequest) -> AnyPublisher { return api.requestPublisher(.signUp(request: request)) diff --git a/SodaLive/Sources/Utils/Constants.swift b/SodaLive/Sources/Utils/Constants.swift index b5d76cd..1987b03 100644 --- a/SodaLive/Sources/Utils/Constants.swift +++ b/SodaLive/Sources/Utils/Constants.swift @@ -27,3 +27,5 @@ let GID_CLIENT_ID = "983594297130-m6bv7lvc1lsetsvv3rk92etqc98uopqj.apps.googleus let GID_SERVER_CLIENT_ID = "983594297130-5hrmkh6vpskeq6v34350kmilf74574h2.apps.googleusercontent.com" let KAKAO_APP_KEY = "231cf78acfa8252fca38b9eedf87c5cb" + +let LINE_CHANNEL_ID = "2008995539"