diff --git a/Atcha-iOS/App/AppConfig.swift b/Atcha-iOS/App/AppConfig.swift
index 0cf35f5f..7e81d874 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 75f79d46..f453f54a 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 = AppConfig.errorWebhookURL
+ private let authWebhookURLString = AppConfig.authWebhookURL
+ // 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/Info.plist b/Atcha-iOS/Info.plist
index 29cd94cd..0cbe917b 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)
diff --git a/Atcha-iOS/Presentation/Location/MainViewController.swift b/Atcha-iOS/Presentation/Location/MainViewController.swift
index c5a7d096..d59b7c80 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
}
@@ -1041,7 +1047,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 {
@@ -1276,8 +1282,8 @@ extension MainViewController {
guard !isShowingToast else { return }
if viewModel.isGuest && guestTapCount == 1 {
- return
- }
+ return
+ }
// [수정] 우리가 정의한 로그인 기반 가이드 로직 적용
let showGuideLine = shouldShowMapGuide
diff --git a/Atcha-iOS/Presentation/Login/LoginViewModel.swift b/Atcha-iOS/Presentation/Login/LoginViewModel.swift
index d26b1a94..39ab529a 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 b3be003b..5ef7f4c7 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 212cd1af..53a0956c 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 d91516f5..ca2f3816 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"]