Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Atcha-iOS/App/AppConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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") }
}
113 changes: 96 additions & 17 deletions Atcha-iOS/Core/Manager/Discord/DiscordWebhookManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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),
Expand All @@ -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")
Expand All @@ -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()
Expand Down
8 changes: 6 additions & 2 deletions Atcha-iOS/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@
<string>$(AMPLITUDE_API_KEY)</string>
<key>API_BASE_URL</key>
<string>$(API_BASE_URL)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>AUTH_WEBHOOK_URL</key>
<string>$(AUTH_WEBHOOK_URL)</string>
<key>ERROR_WEBHOOK_URL</key>
<string>$(ERROR_WEBHOOK_URL)</string>
</dict>
</plist>
12 changes: 9 additions & 3 deletions Atcha-iOS/Presentation/Location/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,12 @@ extension MainViewController {
safeStartJump()

if viewModel.isGuest {
if latestIsServiceRegion == false {
presentLoginAlert()
return
}

// 서비스 지역 내라면 기존의 2-tap 로직(문구 노출 후 로그인) 유지
handleGuestBallonTap()
return
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1276,8 +1282,8 @@ extension MainViewController {
guard !isShowingToast else { return }

if viewModel.isGuest && guestTapCount == 1 {
return
}
return
}

// [수정] 우리가 정의한 로그인 기반 가이드 로직 적용
let showGuideLine = shouldShowMapGuide
Expand Down
6 changes: 6 additions & 0 deletions Atcha-iOS/Presentation/Login/LoginViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions Atcha-iOS/Presentation/User/Home/HomeFindViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 없음")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
8 changes: 8 additions & 0 deletions Atcha-iOS/Presentation/User/Withdraw/WithdrawViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
Loading