Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
48ea4e7
[FEAT] 앱 버전 체크 및 업데이트 유도 팝업 로직 구현
woolnd Mar 26, 2026
bdc5f9c
[FEAT] LaunchType 도입 및 진입 경로별 스플래시 대기 시간 차별화
woolnd Mar 28, 2026
4370b4a
[FEAT] 알람 등록 전에도 캐릭터 터치 시 점프 애니메이션 실행
woolnd Mar 31, 2026
7c1b02b
[BUGFIX] 강제 업데이트 로직 수정
woolnd Mar 31, 2026
afecbeb
[BUGFIX] 토큰 재발급 성공 시 디스코드 알람 코드 삭제
woolnd Mar 31, 2026
8c8a4f9
[BUGFIX] 알람 설정 시 진동 옵션 재선택 시 피드백이 오지 않는 현상 수정
woolnd Mar 31, 2026
f3e9dbe
[BUGFIX] 집 설정 시 마커 라이팅 수정
woolnd Mar 31, 2026
4ab7eec
[FEAT] 게스트 모드 말풍선 터치 인터랙션 및 상태 초기화 로직 개선
woolnd Mar 31, 2026
c4ae611
[FEAT] 말풍선 터치 인터랙션 및 피드백 추가
woolnd Mar 31, 2026
b058c85
[SETTING] 빌드번호 초기화
woolnd Mar 31, 2026
8e70d99
Merge pull request #329 from Atcha-Project/bugfix/#328
woolnd Mar 31, 2026
7dfeadd
[BUGFIX] 말풍선 라이팅 수정
woolnd Mar 31, 2026
20d4d9d
[FEAT] 비서비스 지역에서 게스트가 말풍선 터치 시 로그인 시트 즉시 노출
woolnd Apr 1, 2026
01ebcb7
[FEAT] Discord 웹훅 로그인/로그아웃/회원가입/탈퇴 이벤트 로깅 추가
woolnd Apr 1, 2026
32f8740
Merge pull request #331 from Atcha-Project/bugfix/#328
woolnd Apr 1, 2026
da17950
[FEAT] 디스코드 웹훅 URL 제거 및 xcconfig 이동
woolnd Apr 1, 2026
aa550ab
[FEAT] 게스트 로그인 유도 로직 고도화 및 검색 버튼 활성화 조건 변경
woolnd Apr 1, 2026
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
12 changes: 6 additions & 6 deletions Atcha-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2516,7 +2516,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 23SCTLK482;
FRAMEWORK_SEARCH_PATHS = (
Expand All @@ -2539,7 +2539,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.9.5;
MARKETING_VERSION = 1.9.6;
PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand All @@ -2563,7 +2563,7 @@
CODE_SIGN_ENTITLEMENTS = "Atcha-iOS/Atcha-iOS.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 23SCTLK482;
EXCLUDED_ARCHS = "";
FRAMEWORK_SEARCH_PATHS = (
Expand All @@ -2586,7 +2586,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.9.5;
MARKETING_VERSION = 1.9.6;
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -2611,7 +2611,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 23SCTLK482;
FRAMEWORK_SEARCH_PATHS = (
Expand All @@ -2634,7 +2634,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.9.5;
MARKETING_VERSION = 1.9.6;
PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
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") }
}
11 changes: 7 additions & 4 deletions Atcha-iOS/App/AppFlowCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class AppFlowCoordinator {
self.container = container
}

func startApp() {
func startApp(launchType: LaunchType = .main) {
let navigationController = UINavigationController()
window.rootViewController = navigationController
window.makeKeyAndVisible()
Expand All @@ -35,11 +35,14 @@ class AppFlowCoordinator {
DispatchQueue.main.async {
AppDIContainer.shared.tokenStorage.clearAllTokens()
UserDefaultsWrapper.shared.set(false, forKey: UserDefaultsWrapper.Key.hasSeenIntro.rawValue)
self?.startApp()
self?.startApp(launchType: .fast)
}
}

let splashCoordinator = container.makeSplashCoordinator(navigationController: navigationController)
let splashCoordinator = container.makeSplashCoordinator(
navigationController: navigationController,
launchType: launchType
)
splashCoordinator.routerHandler = { [weak self] router in
guard let self = self else { return }
switch router {
Expand Down Expand Up @@ -114,7 +117,7 @@ class AppFlowCoordinator {
mainCoordinator.withdrawFinish = { [weak self] in
DispatchQueue.main.async {
// 앱 데이터를 다 지웠으니, 스플래시부터 앱을 아예 새로 시작(리부팅)합니다!
self?.startApp()
self?.startApp(launchType: .fast)
}
}

Expand Down
4 changes: 2 additions & 2 deletions Atcha-iOS/App/DIContainer/AppCompositionRoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ extension AppCompositionRoot: SplashCoordinatorFactory,
MainCoordinatorFactory,
LockScreenCoordinatorFactory,
IntroCoordinatorFactory {
func makeSplashCoordinator(navigationController: UINavigationController) -> SplashCoordinator {
return splashDIContainer.makeSplashCoordinator(navigationController: navigationController)
func makeSplashCoordinator(navigationController: UINavigationController, launchType: LaunchType) -> SplashCoordinator {
return splashDIContainer.makeSplashCoordinator(navigationController: navigationController, launchType: launchType)
}

func makeLoginCoordinator(navigationController: UINavigationController) -> LoginCoordinator {
Expand Down
6 changes: 3 additions & 3 deletions Atcha-iOS/App/DIContainer/CoordinatorFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Foundation
/// to avoid service locator style lookups and to enable constructor injection of dependencies.

protocol SplashCoordinatorFactory {
func makeSplashCoordinator(navigationController: UINavigationController) -> SplashCoordinator
func makeSplashCoordinator(navigationController: UINavigationController, launchType: LaunchType) -> SplashCoordinator
}

protocol LoginCoordinatorFactory {
Expand Down Expand Up @@ -45,8 +45,8 @@ extension AppDIContainer: SplashCoordinatorFactory,
MainCoordinatorFactory,
LockScreenCoordinatorFactory,
IntroCoordinatorFactory {
func makeSplashCoordinator(navigationController: UINavigationController) -> SplashCoordinator {
return splashDIContainer.makeSplashCoordinator(navigationController: navigationController)
func makeSplashCoordinator(navigationController: UINavigationController, launchType: LaunchType) -> SplashCoordinator {
return splashDIContainer.makeSplashCoordinator(navigationController: navigationController, launchType: launchType)
}

func makeLoginCoordinator(navigationController: UINavigationController) -> LoginCoordinator {
Expand Down
10 changes: 6 additions & 4 deletions Atcha-iOS/App/DIContainer/Splash/SplashDIContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,23 @@ final class SplashDIContainer {
return UpdateAppVersionUseCaseImpl(repository: repository)
}

func makeSplashViewModel() -> SplashViewModel {
func makeSplashViewModel(launchType: LaunchType) -> SplashViewModel {
return SplashViewModel(
fetchUserUseCase: makeFetchUserUseCase(),
checkAppVersionUseCase: makeCheckAppVersionUseCase(),
updateAppVersionUseCase: makeUpdateAppVersionUseCase(),
tokenStorage: tokenStorage
tokenStorage: tokenStorage,
launchType: launchType
)
}

func makeSplashViewController(viewModel: SplashViewModel) -> SplashViewController {
return SplashViewController(viewModel: viewModel)
}

func makeSplashCoordinator(navigationController: UINavigationController) -> SplashCoordinator {
func makeSplashCoordinator(navigationController: UINavigationController, launchType: LaunchType) -> SplashCoordinator {
SplashCoordinator(navigationController: navigationController,
diContainer: self)
diContainer: self,
launchType: launchType)
}
}
3 changes: 2 additions & 1 deletion Atcha-iOS/App/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
var launchType: LaunchType = .main

appFlowCoordinator = AppFlowCoordinator(window: window, container: diContainer)
appFlowCoordinator?.startApp()
appFlowCoordinator?.startApp(launchType: launchType)

self.window = window
}
Expand Down
4 changes: 2 additions & 2 deletions Atcha-iOS/Base.lproj/LaunchScreen.storyboard
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24504"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
Expand Down
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
2 changes: 2 additions & 0 deletions Atcha-iOS/Core/Network/Token/SessionController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ final class SessionController {
storage.clearRefreshToken()
UserDefaultsWrapper.shared.removeAll()
AppDIContainer.shared.locationStateHolder.clear()
UserDefaultsWrapper.shared.set(true, forKey: UserDefaultsWrapper.Key.isGuest.rawValue)
UserDefaultsWrapper.shared.set(false, forKey: UserDefaultsWrapper.Key.hasSeenIntro.rawValue)

// 로그인 화면으로
DispatchQueue.main.async { [weak self] in
Expand Down
21 changes: 1 addition & 20 deletions Atcha-iOS/Core/Network/Token/TokenInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,6 @@ final class TokenInterceptor: RequestInterceptor, @unchecked Sendable {
return
}

let actualHeaders = request.request?.allHTTPHeaderFields ?? [:]

guard let refreshToken = tokenStorage.refreshToken else {
SessionController.shared.expireAndRouteToLogin()
completion(.doNotRetry)
Expand Down Expand Up @@ -109,24 +107,7 @@ final class TokenInterceptor: RequestInterceptor, @unchecked Sendable {
waiters.forEach { $0(.doNotRetry) }
return
}

let successBody = [
"newAccessToken": p.accessToken,
"newRefreshToken": p.refreshToken
]

DiscordWebhookManager.shared.sendErrorLog(
baseURL: NetworkConstant.baseURL,
statusCode: 200,
method: "GET",
path: "/auth/reissue",
responseCode: "REISSUE_SUCCESS",
message: "토큰 재발급에 성공하여 새로운 토큰을 수신했습니다.",
requestHeaders: actualHeaders,
requestBody: successBody, // 여기서 받은 토큰 정보를 보냅니다.
requestParameters: nil
)


self.tokenStorage.accessToken = p.accessToken
self.tokenStorage.refreshToken = p.refreshToken

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "setting_home_mark.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "setting_home_mark@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "setting_home_mark@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading