From 7dfeadd75662b7983d4b2554fc75884a2309a90d Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Tue, 31 Mar 2026 19:03:13 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[BUGFIX]=20=EB=A7=90=ED=92=8D=EC=84=A0=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=ED=8C=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Atcha-iOS/Presentation/Location/MainViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Atcha-iOS/Presentation/Location/MainViewController.swift b/Atcha-iOS/Presentation/Location/MainViewController.swift index c5a7d09..f034e87 100644 --- a/Atcha-iOS/Presentation/Location/MainViewController.swift +++ b/Atcha-iOS/Presentation/Location/MainViewController.swift @@ -1041,7 +1041,7 @@ extension MainViewController { ballonView.isHidden = false ballonView.alpha = 1 - ballonView.setupTitle(topMessage: nil, bottomMessage: "궁금하면 로그인 해봐요!") + ballonView.setupTitle(topMessage: nil, bottomMessage: "택시비가 궁금하면 로그인해봐요!") ballonView.animateStaggered(secondaryDelay: 0, fade: 0.25) } else { From 20d4d9d1f32e97f9da2b0c682cab49451e5257f3 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Wed, 1 Apr 2026 12:35:05 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[FEAT]=20=EB=B9=84=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=A7=80=EC=97=AD=EC=97=90=EC=84=9C=20=EA=B2=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EA=B0=80=20=EB=A7=90=ED=92=8D=EC=84=A0=20?= =?UTF-8?q?=ED=84=B0=EC=B9=98=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=ED=8A=B8=20=EC=A6=89=EC=8B=9C=20=EB=85=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Location/MainViewController.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Atcha-iOS/Presentation/Location/MainViewController.swift b/Atcha-iOS/Presentation/Location/MainViewController.swift index f034e87..d59b7c8 100644 --- a/Atcha-iOS/Presentation/Location/MainViewController.swift +++ b/Atcha-iOS/Presentation/Location/MainViewController.swift @@ -969,6 +969,12 @@ extension MainViewController { safeStartJump() if viewModel.isGuest { + if latestIsServiceRegion == false { + presentLoginAlert() + return + } + + // 서비스 지역 내라면 기존의 2-tap 로직(문구 노출 후 로그인) 유지 handleGuestBallonTap() return } @@ -1276,8 +1282,8 @@ extension MainViewController { guard !isShowingToast else { return } if viewModel.isGuest && guestTapCount == 1 { - return - } + return + } // [수정] 우리가 정의한 로그인 기반 가이드 로직 적용 let showGuideLine = shouldShowMapGuide From 01ebcb74b1de1dbd5c286b355f1efa76d3e59c34 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Wed, 1 Apr 2026 12:59:15 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[FEAT]=20Discord=20=EC=9B=B9=ED=9B=85=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8/=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83/=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85/=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Discord/DiscordWebhookManager.swift | 113 +++++++++++++++--- .../Presentation/Login/LoginViewModel.swift | 6 + .../User/Home/HomeFindViewModel.swift | 6 + .../User/MyAccount/MyAccountViewModel.swift | 9 +- .../User/Withdraw/WithdrawViewModel.swift | 8 ++ 5 files changed, 124 insertions(+), 18 deletions(-) diff --git a/Atcha-iOS/Core/Manager/Discord/DiscordWebhookManager.swift b/Atcha-iOS/Core/Manager/Discord/DiscordWebhookManager.swift index 75f79d4..8c1de32 100644 --- a/Atcha-iOS/Core/Manager/Discord/DiscordWebhookManager.swift +++ b/Atcha-iOS/Core/Manager/Discord/DiscordWebhookManager.swift @@ -11,8 +11,10 @@ final class DiscordWebhookManager { static let shared = DiscordWebhookManager() private init() {} - private let webhookURLString = "https://discord.com/api/webhooks/1483870710018474066/qyzNBI1Bwr7J5tQDrPx2-mOcej_9yLSOk5Bmlmza2D-4nSWqvWgcMd4CZDziG4vkpKrm" + private let errorWebhookURLString = "https://discord.com/api/webhooks/1483870710018474066/qyzNBI1Bwr7J5tQDrPx2-mOcej_9yLSOk5Bmlmza2D-4nSWqvWgcMd4CZDziG4vkpKrm" + private let authWebhookURLString = "https://discord.com/api/webhooks/1488745616485126185/AXfHS732U9-Oo3iMgicAitZh-oNnjE8EAUVapWxg38tmyCpjuHd8R3BaxbcSEr82Y_qu" + // MARK: - 오류 로그 func sendErrorLog( baseURL: String, statusCode: Int, @@ -24,14 +26,10 @@ final class DiscordWebhookManager { requestBody: [String: Any]? = nil, requestParameters: [String: Any]? = nil ) { - guard let url = URL(string: webhookURLString) else { return } + guard let url = URL(string: errorWebhookURLString) else { return } - // Authorization 토큰 앞 30자만 노출 - let headersText = requestHeaders.map { key, value in - return "\(key): \(value)" - }.joined(separator: "\n") + let headersText = requestHeaders.map { "\($0.key): \($0.value)" }.joined(separator: "\n") - // body JSON 변환 let bodyText: String if let body = requestBody, let data = try? JSONSerialization.data(withJSONObject: body, options: .prettyPrinted), @@ -54,22 +52,58 @@ final class DiscordWebhookManager { "content": "🚨 [Atcha-iOS] API 에러 발생!", "embeds": [[ "title": "서버 에러 상세 보고", - "color": 16711680, + "color": 16711680, // 빨강 "fields": [ - ["name": "Base URL", "value": "`\(baseURL)`", "inline": false], - ["name": "Method & Path", "value": "`\(method) \(path)`", "inline": false], - ["name": "HTTP Status", "value": "\(statusCode)", "inline": true], - ["name": "responseCode", "value": responseCode, "inline": true], - ["name": "App Version", "value": AppInfoProvider.currentVersion, "inline": true], - ["name": "Error Message", "value": message, "inline": false], - ["name": "Request Headers", "value": "```\n\(headersText)\n```", "inline": false], - ["name": "Request Parameters", "value": paramsText, "inline": false], - ["name": "Request Body", "value": bodyText, "inline": false] + ["name": "Base URL", "value": "`\(baseURL)`", "inline": false], + ["name": "Method & Path", "value": "`\(method) \(path)`", "inline": false], + ["name": "HTTP Status", "value": "\(statusCode)", "inline": true], + ["name": "responseCode", "value": responseCode, "inline": true], + ["name": "App Version", "value": AppInfoProvider.currentVersion, "inline": true], + ["name": "Error Message", "value": message, "inline": false], + ["name": "Request Headers", "value": "```\n\(headersText)\n```", "inline": false], + ["name": "Request Parameters", "value": paramsText, "inline": false], + ["name": "Request Body", "value": bodyText, "inline": false] ], "footer": ["text": "발생 시각: \(Date().kstString)"] ]] ] + sendToWebhook(url: url, payload: payload) + } + + // MARK: - 로그인/탈퇴 로그 + func sendAuthLog(event: AuthEvent, userID: String, provider: String? = nil, reason: String? = nil) { + guard let url = URL(string: authWebhookURLString) else { return } + + var fields: [[String: Any]] = [ + ["name": "이벤트", "value": event.title, "inline": true], + ["name": "유저 ID", "value": "`\(userID)`", "inline": true], + ["name": "App Version", "value": AppInfoProvider.currentVersion, "inline": true] + ] + + if let provider { + fields.append(["name": "로그인 방식", "value": provider, "inline": true]) + } + + if let reason { + fields.append(["name": "탈퇴 사유", "value": reason, "inline": false]) + } + + let payload: [String: Any] = [ + "content": event.headerMessage, + "embeds": [[ + "title": event.embedTitle, + "color": event.color, + "fields": fields, + "footer": ["text": "발생 시각: \(Date().kstString)"] + ]] + ] + + sendToWebhook(url: url, payload: payload) + } + + // MARK: - 공통 전송 + private func sendToWebhook(url: URL, payload: [String: Any]) { var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") @@ -79,6 +113,51 @@ final class DiscordWebhookManager { } } +// MARK: - Auth Event 타입 +enum AuthEvent { + case login + case signup + case logout + case withdraw + + var title: String { + switch self { + case .login: return "로그인" + case .signup: return "회원가입" + case .logout: return "로그아웃" + case .withdraw: return "회원탈퇴" + } + } + + var embedTitle: String { + switch self { + case .login: return "로그인 이벤트" + case .signup: return "회원가입 이벤트" + case .logout: return "로그아웃 이벤트" + case .withdraw: return "회원탈퇴 이벤트" + } + } + + var headerMessage: String { + switch self { + case .login: return "✅ [Atcha-iOS] 로그인" + case .signup: return "🎉 [Atcha-iOS] 회원가입" + case .logout: return "👋 [Atcha-iOS] 로그아웃" + case .withdraw: return "❌ [Atcha-iOS] 회원탈퇴" + } + } + + var color: Int { + switch self { + case .login: return 3066993 // 초록 + case .signup: return 5814783 // 파랑 + case .logout: return 16776960 // 노랑 + case .withdraw: return 10038562 // 보라 + } + } +} + +// MARK: - Date Extension private extension Date { var kstString: String { let formatter = DateFormatter() diff --git a/Atcha-iOS/Presentation/Login/LoginViewModel.swift b/Atcha-iOS/Presentation/Login/LoginViewModel.swift index d26b1a9..39ab529 100644 --- a/Atcha-iOS/Presentation/Login/LoginViewModel.swift +++ b/Atcha-iOS/Presentation/Login/LoginViewModel.swift @@ -74,6 +74,12 @@ extension LoginViewModel { AmplitudeManager.shared.bindUser(id: String(id)) AmplitudeManager.shared.flush() + + DiscordWebhookManager.shared.sendAuthLog( + event: .login, + userID: String(id), + provider: type == .kakao ? "카카오" : "애플" + ) } UserDefaultsWrapper.shared.set(false, forKey: UserDefaultsWrapper.Key.isGuest.rawValue) diff --git a/Atcha-iOS/Presentation/User/Home/HomeFindViewModel.swift b/Atcha-iOS/Presentation/User/Home/HomeFindViewModel.swift index b3be003..5ef7f4c 100644 --- a/Atcha-iOS/Presentation/User/Home/HomeFindViewModel.swift +++ b/Atcha-iOS/Presentation/User/Home/HomeFindViewModel.swift @@ -274,6 +274,12 @@ extension HomeFindViewModel { AmplitudeManager.shared.track( .signup ) + + DiscordWebhookManager.shared.sendAuthLog( + event: .signup, + userID: String(id), + provider: provider == 0 ? "카카오" : "애플" + ) } else { print("회원가입 응답에 lat/lon 없음") } diff --git a/Atcha-iOS/Presentation/User/MyAccount/MyAccountViewModel.swift b/Atcha-iOS/Presentation/User/MyAccount/MyAccountViewModel.swift index 212cd1a..53a0956 100644 --- a/Atcha-iOS/Presentation/User/MyAccount/MyAccountViewModel.swift +++ b/Atcha-iOS/Presentation/User/MyAccount/MyAccountViewModel.swift @@ -27,10 +27,17 @@ final class MyAccountViewModel: BaseViewModel { Task { do { let _ = try await logoutUseCase.excute() + + let userId = UserDefaultsWrapper.shared.integer(forKey: UserDefaultsWrapper.Key.userId.rawValue) ?? 0 + DiscordWebhookManager.shared.sendAuthLog( + event: .logout, + userID: String(userId) + ) + AmplitudeManager.shared.track(.logout) AmplitudeManager.shared.reset() - tokenStorage.clearAllTokens() + tokenStorage.clearAllTokens() UserDefaultsWrapper.shared.removeAll() locationStateHolder.clear() diff --git a/Atcha-iOS/Presentation/User/Withdraw/WithdrawViewModel.swift b/Atcha-iOS/Presentation/User/Withdraw/WithdrawViewModel.swift index d91516f..ca2f381 100644 --- a/Atcha-iOS/Presentation/User/Withdraw/WithdrawViewModel.swift +++ b/Atcha-iOS/Presentation/User/Withdraw/WithdrawViewModel.swift @@ -28,6 +28,14 @@ final class WithdrawViewModel: BaseViewModel { let reason = request.reason? .trimmingCharacters(in: .whitespacesAndNewlines) + let userId = UserDefaultsWrapper.shared.integer(forKey: UserDefaultsWrapper.Key.userId.rawValue) ?? 0 + + DiscordWebhookManager.shared.sendAuthLog( + event: .withdraw, + userID: String(userId), + reason: reason + ) + AmplitudeManager.shared.track( .withdraw, [AmplitudePropertyKey.withdrawReason.rawValue: reason ?? "unknown"] From da179505b895143daf243dea8238f8f1e8384612 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Wed, 1 Apr 2026 13:33:41 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[FEAT]=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9B=B9=ED=9B=85=20URL=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=8F=20xcconfig=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Atcha-iOS/App/AppConfig.swift | 2 ++ .../Core/Manager/Discord/DiscordWebhookManager.swift | 4 ++-- Atcha-iOS/Info.plist | 8 ++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Atcha-iOS/App/AppConfig.swift b/Atcha-iOS/App/AppConfig.swift index 0cf35f5..7e81d87 100644 --- a/Atcha-iOS/App/AppConfig.swift +++ b/Atcha-iOS/App/AppConfig.swift @@ -21,4 +21,6 @@ enum AppConfig { static var kakaoInitKey: String { required("KAKAO_INIT_KEY") } static var tmapApiKey: String { required("TMAP_API_KEY") } static var amplitudeApiKey: String { required("AMPLITUDE_API_KEY") } + static var errorWebhookURL: String { required("ERROR_WEBHOOK_URL") } + static var authWebhookURL: String { required("AUTH_WEBHOOK_URL") } } diff --git a/Atcha-iOS/Core/Manager/Discord/DiscordWebhookManager.swift b/Atcha-iOS/Core/Manager/Discord/DiscordWebhookManager.swift index 8c1de32..f453f54 100644 --- a/Atcha-iOS/Core/Manager/Discord/DiscordWebhookManager.swift +++ b/Atcha-iOS/Core/Manager/Discord/DiscordWebhookManager.swift @@ -11,8 +11,8 @@ final class DiscordWebhookManager { static let shared = DiscordWebhookManager() private init() {} - private let errorWebhookURLString = "https://discord.com/api/webhooks/1483870710018474066/qyzNBI1Bwr7J5tQDrPx2-mOcej_9yLSOk5Bmlmza2D-4nSWqvWgcMd4CZDziG4vkpKrm" - private let authWebhookURLString = "https://discord.com/api/webhooks/1488745616485126185/AXfHS732U9-Oo3iMgicAitZh-oNnjE8EAUVapWxg38tmyCpjuHd8R3BaxbcSEr82Y_qu" + private let errorWebhookURLString = AppConfig.errorWebhookURL + private let authWebhookURLString = AppConfig.authWebhookURL // MARK: - 오류 로그 func sendErrorLog( diff --git a/Atcha-iOS/Info.plist b/Atcha-iOS/Info.plist index 29cd94c..0cbe917 100644 --- a/Atcha-iOS/Info.plist +++ b/Atcha-iOS/Info.plist @@ -67,7 +67,11 @@ $(AMPLITUDE_API_KEY) API_BASE_URL $(API_BASE_URL) - ITSAppUsesNonExemptEncryption - + ITSAppUsesNonExemptEncryption + + AUTH_WEBHOOK_URL + $(AUTH_WEBHOOK_URL) + ERROR_WEBHOOK_URL + $(ERROR_WEBHOOK_URL)