From 799a512a46326276e8afd611372193ffdfc9bf1d Mon Sep 17 00:00:00 2001 From: Roy Date: Thu, 18 Dec 2025 12:30:00 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[feat]:=20mixpanel=20Analytics=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Data/Project.swift | 1 + .../Analytics/AnalyticsRepository.swift} | 35 +++++++++++-------- .../Sources/Application/AppDelegate.swift | 5 ++- .../Application/LiveDependencies.swift | 2 +- Tuist/Package.swift | 6 ++-- .../Environment.swift | 2 +- .../InfoPlist+Helpers.swift | 1 + .../TargetDependency+SPM.swift | 1 + fastlane/metadata/ko/release_notes.txt | 6 ++-- 9 files changed, 36 insertions(+), 23 deletions(-) rename Data/Sources/{Analytics/FirebaseAnalyticsManager.swift => Repository/Analytics/AnalyticsRepository.swift} (77%) diff --git a/Data/Project.swift b/Data/Project.swift index 0cf8652e..bcc5b87a 100644 --- a/Data/Project.swift +++ b/Data/Project.swift @@ -10,6 +10,7 @@ let project = Project.makeFramework( .SPM.GoogleSignIn, .SPM.FirebaseAnalytics, .SPM.FirebaseCrashlytics, + .SPM.Mixpanel ], hasTests: true, settings: .settings( diff --git a/Data/Sources/Analytics/FirebaseAnalyticsManager.swift b/Data/Sources/Repository/Analytics/AnalyticsRepository.swift similarity index 77% rename from Data/Sources/Analytics/FirebaseAnalyticsManager.swift rename to Data/Sources/Repository/Analytics/AnalyticsRepository.swift index 9e15aa09..1a6271b6 100644 --- a/Data/Sources/Analytics/FirebaseAnalyticsManager.swift +++ b/Data/Sources/Repository/Analytics/AnalyticsRepository.swift @@ -2,9 +2,10 @@ import Foundation import FirebaseAnalytics import Domain import LogMacro +import Mixpanel /// FirebaseAnalytics를 사용한 Analytics Repository 구현체 -public class FirebaseAnalyticsRepository: AnalyticsRepositoryProtocol, @unchecked Sendable { +public class AnalyticsRepository: AnalyticsRepositoryProtocol, @unchecked Sendable { public init() { #logDebug("🔥 [Analytics] ===== FIREBASE ANALYTICS REPOSITORY INITIALIZED =====") @@ -34,11 +35,12 @@ public class FirebaseAnalyticsRepository: AnalyticsRepositoryProtocol, @unchecke private func sendAuthEvent(_ eventType: AuthEventType, _ data: AuthEventData) async { var params: [String: Any] = ["social_type": data.socialType] if let isFirst = data.isFirst { - params["is_first"] = isFirst + params["is_first"] = isFirst as Any } #logDebug("🔥 [Analytics] Parameters: \(params)") Analytics.logEvent(eventType.rawValue, parameters: params) + Mixpanel.mainInstance().track(event: eventType.rawValue, properties: params as? [String: MixpanelType]) } private func sendDeeplinkEvent(_ data: DeeplinkEventData) async { @@ -50,36 +52,39 @@ public class FirebaseAnalyticsRepository: AnalyticsRepositoryProtocol, @unchecke #logDebug("🔥 [Analytics] Parameters: \(parameters)") Analytics.logEvent("deeplink_open", parameters: parameters) #logDebug("🔥 [Analytics] ✅ deeplink_open event sent to Firebase") + Mixpanel.mainInstance().track(event: "deeplink_open", properties: parameters as? [String: MixpanelType]) } private func sendTravelEvent(_ eventType: TravelEventType, _ data: TravelEventData) async { var params: [String: Any] = ["travel_id": data.travelId] // Optional fields based on event type - if let userId = data.userId { params["user_id"] = userId } - if let memberId = data.memberId { params["member_id"] = memberId } - if let role = data.role { params["role"] = role } - if let newOwnerId = data.newOwnerId { params["new_owner_id"] = newOwnerId } + if let userId = data.userId { params["user_id"] = userId as Any } + if let memberId = data.memberId { params["member_id"] = memberId as Any } + if let role = data.role { params["role"] = role as Any } + if let newOwnerId = data.newOwnerId { params["new_owner_id"] = newOwnerId as Any } #logDebug("🔥 [Analytics] Parameters: \(params)") Analytics.logEvent(eventType.rawValue, parameters: params) + Mixpanel.mainInstance().track(event: eventType.rawValue, properties: params as? [String: MixpanelType]) } private func sendExpenseEvent(_ eventType: ExpenseEventType, _ data: ExpenseEventData) async { var params: [String: Any] = ["travel_id": data.travelId] // Add optional fields - if let expenseId = data.expenseId { params["expense_id"] = expenseId } - if let amount = data.amount { params["amount"] = amount } - if let currency = data.currency { params["currency"] = currency } - if let category = data.category { params["category"] = category } - if let payerId = data.payerId { params["payer_id"] = payerId } - if let source = data.source { params["source"] = source } - if let tab = data.tab { params["tab"] = tab } - if let expenseDate = data.expenseDate { params["expense_date"] = expenseDate } - if let errorCode = data.errorCode { params["error_code"] = errorCode } + if let expenseId = data.expenseId { params["expense_id"] = expenseId as Any } + if let amount = data.amount { params["amount"] = amount as Any } + if let currency = data.currency { params["currency"] = currency as Any } + if let category = data.category { params["category"] = category as Any } + if let payerId = data.payerId { params["payer_id"] = payerId as Any } + if let source = data.source { params["source"] = source as Any } + if let tab = data.tab { params["tab"] = tab as Any } + if let expenseDate = data.expenseDate { params["expense_date"] = expenseDate as Any } + if let errorCode = data.errorCode { params["error_code"] = errorCode as Any } #logDebug("🔥 [Analytics] Parameters: \(params)") Analytics.logEvent(eventType.rawValue, parameters: params) + Mixpanel.mainInstance().track(event: eventType.rawValue, properties: params as? [String: MixpanelType]) } } diff --git a/SseuDamApp/Sources/Application/AppDelegate.swift b/SseuDamApp/Sources/Application/AppDelegate.swift index 2471683c..0586d5e6 100644 --- a/SseuDamApp/Sources/Application/AppDelegate.swift +++ b/SseuDamApp/Sources/Application/AppDelegate.swift @@ -12,10 +12,12 @@ import Data import Firebase import FirebaseAnalytics import ComposableArchitecture +import Mixpanel final class AppDelegate: NSObject, UIApplicationDelegate, @MainActor UNUserNotificationCenterDelegate { @Dependency(\.deeplinkRouter) var deeplinkRouter - + let mixPanelKey = Bundle.main.object(forInfoDictionaryKey: "MIXPANEL_TOKEN") as? String + @MainActor func application( _ application: UIApplication, @@ -28,6 +30,7 @@ final class AppDelegate: NSObject, UIApplicationDelegate, @MainActor UNUserNotif #endif FirebaseApp.configure() + Mixpanel.initialize(token: mixPanelKey ?? "", trackAutomaticEvents: false) Analytics.setAnalyticsCollectionEnabled(true) let center = UNUserNotificationCenter.current() center.delegate = self diff --git a/SseuDamApp/Sources/Application/LiveDependencies.swift b/SseuDamApp/Sources/Application/LiveDependencies.swift index f1744e9c..df6e2841 100644 --- a/SseuDamApp/Sources/Application/LiveDependencies.swift +++ b/SseuDamApp/Sources/Application/LiveDependencies.swift @@ -25,7 +25,7 @@ public enum LiveDependencies { ) dependencies.sessionStoreRepository = SessionStoreRepository() // Analytics - dependencies.analyticsRepository = FirebaseAnalyticsRepository() + dependencies.analyticsRepository = AnalyticsRepository() // Travel dependencies.travelRepository = TravelRepository( diff --git a/Tuist/Package.swift b/Tuist/Package.swift index e1727f5e..f6607dcf 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -18,7 +18,8 @@ let packageSettings = PackageSettings( "IssueReportingPackageSupport": .framework, "XCTestDynamicOverlay": .framework, "Clocks": .framework, - "ConcurrencyExtras": .framework + "ConcurrencyExtras": .framework, + "Mixpanel": .framework // "FirebaseCore": .staticLibrary, // "FirebaseAuth": .staticLibrary, // "FirebaseFirestore": .staticLibrary, @@ -40,6 +41,7 @@ let package = Package( .package(url: "https://github.com/Roy-wonji/LogMacro.git", from: "1.1.1"), .package(url: "https://github.com/Moya/Moya.git", exact: "15.0.3"), .package(url: "https://github.com/firebase/firebase-ios-sdk.git", exact: "12.7.0"), - .package(url: "https://github.com/openid/AppAuth-iOS.git", from: "2.0.0") + .package(url: "https://github.com/openid/AppAuth-iOS.git", from: "2.0.0"), + .package(url: "https://github.com/mixpanel/mixpanel-swift.git", from: "5.1.3") ] ) diff --git a/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/Environment.swift b/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/Environment.swift index ce7f6d1c..7c23b3a6 100644 --- a/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/Environment.swift +++ b/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/Environment.swift @@ -10,7 +10,7 @@ public enum Environment { // MARK: - Version Management public static let mainAppVersion = "1.0.4" - public static let mainAppBuildVersion = "24" + public static let mainAppBuildVersion = "26" public static let demoAppVersion = "0.1.0" // Demo app용 별도 버전 // MARK: - Platform diff --git a/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/InfoPlist+Helpers.swift b/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/InfoPlist+Helpers.swift index 5588d29e..60dbdcce 100644 --- a/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/InfoPlist+Helpers.swift +++ b/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/InfoPlist+Helpers.swift @@ -14,6 +14,7 @@ public extension InfoPlist { "GOOGLE_REVERSED_IOS_CLIENT_ID": .string("$(GOOGLE_REVERSED_IOS_CLIENT_ID)"), "KAKAO_NATIVE_APP_KEY": .string("$(KAKAO_NATIVE_APP_KEY)"), "KAKAO_REST_API_KEY": .string("$(KAKAO_REST_API_KEY)"), + "MIXPANEL_TOKEN": .string("$(MIXPANEL_TOKEN)"), "CFBundleURLTypes": .array([ .dictionary([ "CFBundleURLName": .string("sseudam"), diff --git a/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/TargetDependency+SPM.swift b/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/TargetDependency+SPM.swift index d48e582b..d18758d3 100644 --- a/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/TargetDependency+SPM.swift +++ b/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/TargetDependency+SPM.swift @@ -19,4 +19,5 @@ public extension TargetDependency.SPM { static let LogMacro: TargetDependency = .external(name: "LogMacro") static let FirebaseAnalytics = TargetDependency.external(name: "FirebaseAnalytics", condition: .none) static let FirebaseCrashlytics = TargetDependency.external(name: "FirebaseCrashlytics", condition: .none) + static let Mixpanel = TargetDependency.external(name: "Mixpanel", condition: .none) } diff --git a/fastlane/metadata/ko/release_notes.txt b/fastlane/metadata/ko/release_notes.txt index f4de8c92..e40b26dc 100644 --- a/fastlane/metadata/ko/release_notes.txt +++ b/fastlane/metadata/ko/release_notes.txt @@ -1,5 +1,5 @@ -[v 1.0.3] +[v 1.0.4] - 버그 수정 -- 정산 내역 추가 -- 여행 정산 페이지수정 +- 정산 내역 차트 추가 +- 디자인 수 From 5bc48471b133982788ba79a2d5b5a2134942cbba Mon Sep 17 00:00:00 2001 From: Roy Date: Thu, 18 Dec 2025 15:42:15 +0900 Subject: [PATCH 2/2] docs: Read me update --- README.md | 208 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 165 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index b71e855f..af033819 100644 --- a/README.md +++ b/README.md @@ -14,21 +14,39 @@ ## 📱 주요 기능 -### 🌍 여행 관리 -- **여행 생성 및 관리**: 여행 일정과 참가자 관리 +### 🌍 **여행 관리** +- **여행 생성 및 관리**: 여행 일정과 참가자 관리 (김민희 담당) - **초대 코드**: 친구들을 쉽게 여행에 초대 +- **여행 참여/나가기**: 실시간 멤버 관리 시스템 - **실시간 환율**: 최신 환율 정보로 정확한 경비 계산 -### 💰 경비 관리 & 정산 +### 💰 **경비 관리 & 정산** +- **지출 내역 CRUD**: 지출 조회, 생성, 수정, 삭제 (홍석현 담당) +- **카테고리별 분류**: 식비, 숙박, 교통, 쇼핑 등 체계적 분류 - **실시간 경비 입력**: 여행 중 발생하는 모든 경비 기록 -- **자동 정산 계산**: 복잡한 N분의 1 정산을 자동으로 계산 -- **정산 내역 공유**: 투명한 정산 결과 공유 - -### 🔐 간편 인증 -- **소셜 로그인**: Google, Apple 로그인 지원 +- **스마트 정산**: Pay(지출액) - Owe(책임액) 자동 계산으로 명확한 정산 제안 +- **정산 내역 상세보기**: 멤버별 투명한 정산 내역 공개 +- **일별 지출 차트**: 터치 인터랙션이 가능한 시각화 그래프 + +### 🔐 **인증 시스템** +- **소셜 로그인**: Google, Apple 로그인 지원 (서원지 담당) +- **스플래시 화면**: 부드러운 앱 시작 경험 +- **온보딩**: 신규 사용자를 위한 친절한 가이드 - **안전한 인증**: OAuth 2.0 기반 보안 시스템 -### 🎨 사용자 경험 +### 👤 **프로필 관리** +- **프로필 수정**: 사용자 정보 관리 (서원지 담당) +- **이미지 캐싱**: ImageCacheService로 최적화된 프로필 이미지 +- **버전 정보**: 앱 버전 표시 및 관리 + +### 📊 **Analytics & 추적** +- **이중 트래킹**: Firebase Analytics + Mixpanel 동시 수집 +- **사용자 행동 분석**: 앱 사용 패턴 및 성능 모니터링 +- **딥링크 추적**: 앱 진입 경로 분석 + +### 🎨 **사용자 경험** +- **아름다운 디자인**: 전문 디자이너의 UI/UX 설계 (오나람 담당) +- **커스텀 아이콘**: 브랜드 일관성을 위한 전용 아이콘 디자인 - **Skeleton Loading**: 부드러운 로딩 경험 - **Toast 알림**: 직관적인 피드백 시스템 - **다크 모드**: 시스템 설정에 따른 테마 자동 변경 @@ -40,52 +58,115 @@ ``` SseuDam/ -├── 🎯 Features/ # 기능별 모듈 -│ ├── Login/ # 로그인 & 회원가입 -│ ├── Travel/ # 여행 관리 -│ ├── Expense/ # 경비 관리 -│ ├── Settlement/ # 정산 -│ ├── Profile/ # 프로필 -│ ├── Web/ # WebView 기능 -│ ├── Main/ # 메인 탭 -│ └── Splash/ # 스플래시 +├── 🎯 Features/ # 기능별 모듈 +│ ├── Login/ # 로그인 & 소셜 인증 (서원지) +│ ├── OnBoarding/ # 신규 사용자 가이드 +│ ├── Splash/ # 스플래시 화면 (서원지) +│ ├── Profile/ # 프로필 관리 (서원지) +│ ├── Travel/ # 여행 CRUD & 멤버 관리 (김민희) +│ ├── Member/ # 여행 참여/나가기 (김민희) +│ ├── Expense/ # 지출 등록 +│ ├── ExpenseList/ # 지출 내역 & 차트 (홍석현) +│ ├── SaveExpense/ # 지출 저장 로직 +│ ├── Settlement/ # 정산 메인 (홍석현) +│ ├── SettlementResult/ # 정산 결과 (홍석현) +│ ├── SettlementDetail/ # 정산 상세보기 (홍석현) +│ ├── Main/ # 메인 탭 네비게이션 +│ └── Web/ # WebView 기능 │ ├── 🏛️ Core Layers/ -│ ├── Domain/ # 비즈니스 로직 & 엔터티 -│ ├── Data/ # Repository 구현 & DTO -│ ├── NetworkService/ # API 통신 -│ └── DesignSystem/ # UI 컴포넌트 & 디자인 시스템 +│ ├── Domain/ # 비즈니스 로직 & 엔터티 +│ ├── Data/ # Repository 구현 & DTO +│ ├── NetworkService/ # API 통신 +│ └── DesignSystem/ # UI 컴포넌트 & 디자인 시스템 (오나람) +│ +├── 📱 App/ +│ └── SseuDamApp/ # 메인 애플리케이션 │ -└── 📱 App/ - └── SseuDamApp/ # 메인 애플리케이션 +├── ⚙️ Configuration/ +│ ├── Config/ # 환경 설정 +│ ├── Entitlements/ # 앱 권한 설정 +│ └── Scripts/ # 빌드 스크립트 +│ +└── 🧪 Tests/ # 테스트 모음 ``` ### 기술 스택 -#### 🏗️ 아키텍처 & 패턴 -- **[TCA (The Composable Architecture)](https://github.com/pointfreeco/swift-composable-architecture)**: 단방향 데이터 플로우 -- **[TCACoordinators](https://github.com/johnpatrickmorgan/TCACoordinators)**: 네비게이션 관리 +#### 🏗️ **아키텍처 & 패턴** +- **[TCA (The Composable Architecture)](https://github.com/pointfreeco/swift-composable-architecture)**: 단방향 데이터 플로우, 상태 관리 +- **[TCACoordinators](https://github.com/johnpatrickmorgan/TCACoordinators)**: 네비게이션 관리 (홍석현 담당) - **Clean Architecture**: 계층 분리 및 의존성 역전 -- **[Tuist](https://tuist.io/)**: 프로젝트 생성 및 모듈 관리 +- **[Tuist](https://tuist.io/)**: 프로젝트 생성 및 모듈 관리 (홍석현 담당) +- **Dependencies 시스템**: @Dependency를 활용한 DI 간소화 -#### 🌐 네트워킹 -- **[Supabase](https://supabase.com/)**: Backend as a Service +#### 🌐 **네트워킹 & 백엔드** +- **[Supabase](https://supabase.com/)**: Backend as a Service (서원지 담당) - **[Moya](https://github.com/Moya/Moya)**: 타입 세이프한 네트워킹 - **[Alamofire](https://github.com/Alamofire/Alamofire)**: HTTP 네트워킹 +- **실시간 환율 API**: 정확한 통화 변환 -#### 🔐 인증 +#### 🔐 **인증 & 보안** - **[Google Sign-In](https://developers.google.com/identity/sign-in/ios)**: Google OAuth +- **[Apple Sign-In](https://developer.apple.com/sign-in-with-apple/)**: Apple 로그인 - **[AppAuth-iOS](https://github.com/openid/AppAuth-iOS)**: OAuth 2.0 / OpenID Connect +- **Keychain Services**: 보안 토큰 저장 -#### 🎨 UI/UX -- **SwiftUI**: 선언적 UI 프레임워크 -- **Skeleton Loading**: 로딩 상태 UX -- **Toast System**: 사용자 피드백 +#### 📊 **Analytics & 모니터링** +- **[Firebase Analytics](https://firebase.google.com/products/analytics)**: 사용자 행동 분석 +- **[Mixpanel](https://mixpanel.com/)**: 고급 이벤트 추적 +- **이중 트래킹**: 데이터 정확도 향상을 위한 이중 수집 시스템 -#### 🛠️ 개발 도구 -- **[FastLane](https://fastlane.tools/)**: 배포 자동화 +#### 💾 **데이터 관리** +- **SwiftData**: iOS 17+ 로컬 데이터 저장 +- **UserDefaults**: 사용자 설정 관리 +- **ImageCacheService**: NSCache 기반 이미지 캐싱 (50MB 제한) +- **AsyncStream**: 여행 리스트 실시간 캐시 처리 + +#### 🎨 **UI/UX** +- **SwiftUI**: 선언적 UI 프레임워크 +- **AsyncImage**: 비동기 이미지 로딩 +- **Skeleton Loading**: 로딩 상태 UX 개선 +- **Toast System**: 사용자 피드백 향상 +- **Custom Design System**: 일관된 브랜드 경험 (오나람 디자인) + +#### 🛠️ **개발 도구 & 배포** +- **[Fastlane](https://fastlane.tools/)**: 배포 자동화 +- **GitHub Actions**: QA 자동 빌드 시스템 +- **TestFlight**: 베타 테스트 배포 - **Xcode 16.0+**: iOS 개발 환경 +## 👥 팀 구성 + +### 💻 **개발팀** + +#### 🎨 **김민희** - iOS 개발자 +- **여행 관리 시스템**: 여행 조회, 생성, 수정, 삭제 +- **멤버십 관리**: 여행 참여, 나가기 기능 +- **여행 멤버 관리**: 실시간 멤버 상태 관리 +- **캐시 시스템**: SwiftData 기반 데이터 저장 최적화 + +#### 🏗️ **홍석현** - iOS 개발자 & 아키텍트 +- **프로젝트 구조 설계**: Clean Architecture + TCA 설계 +- **지출 관리 시스템**: 지출 내역 CRUD 전체 구현 +- **TCACoordinator**: 화면 전환 및 네비게이션 관리 +- **정산 시스템**: 스마트 정산 계산 및 상세 내역 구현 +- **시각화**: 일별 지출 차트 및 인터랙션 구현 + +#### 🌐 **서원지** - 풀스택 개발자 +- **서버 개발**: Supabase 기반 백엔드 API 설계 및 구현 +- **인증 시스템**: 스플래시, 회원가입, 로그인 전체 플로우 +- **프로필 관리**: 사용자 정보 관리 및 이미지 캐싱 시스템 +- **Analytics 시스템**: Firebase + Mixpanel 이중 트래킹 구현 +- **보안**: OAuth 2.0 기반 안전한 인증 구현 + +### 🎨 **디자인팀** + +#### 🌈 **오나람** - UI/UX 디자이너 +- **앱 디자인**: 전체 UI/UX 설계 및 디자인 시스템 구축 +- **브랜드 아이덴티티**: 아이콘 디자인 및 시각적 일관성 구축 +- **사용자 경험**: 직관적이고 아름다운 인터페이스 설계 + ## 🚀 시작하기 ### 필수 요구사항 @@ -157,9 +238,9 @@ tuist build tuist build -c release ``` -### FastLane을 통한 배포 +### QA 배포 ```bash -# QA 빌드 (TestFlight) +# 수동 실행 fastlane QA ``` @@ -171,14 +252,55 @@ fastlane QA - **Swift Style Guide**: [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/) 준수 - **접근 제어자**: `private`, `fileprivate`, `internal`, `public` 적극 활용 - **final 키워드**: 상속이 불필요한 클래스에 `final` 사용 +- **타입 안전성**: `as Any` 명시적 캐스팅으로 타입 모호성 방지 + +#### 아키텍처 패턴 +- **TCA**: State, Action, Reducer 패턴 준수 +- **의존성 주입**: `@Dependency` 활용한 DI 시스템 +- **모듈 분리**: Feature별 독립적인 모듈 구조 +### 🔧 주요 시스템 + +#### 정산 시스템 +- **Pay - Owe 알고리즘**: 각 멤버의 순 차액 자동 계산 +- **투명한 정산**: 멤버별 결제/부담 내역 상세 공개 +- **시각화**: 일별 지출 차트로 지출 패턴 파악 + +#### 캐시 시스템 +- **SwiftData**: JSON 파일 방식에서 SwiftData로 전환 +- **ImageCacheService**: Actor 패턴으로 thread-safe 이미지 캐싱 +- **AsyncStream**: 실시간 데이터 스트리밍 + +#### Analytics 시스템 +- **이중 트래킹**: Firebase + Mixpanel 동시 수집 +- **데이터 비교**: 두 플랫폼의 분석 결과 비교로 최적 솔루션 선택 ## 🛠️ 요구사항 -- Xcode 16.0+ -- iOS 17.0+ -- Tuist 4.0+ +- **Xcode 16.0+** +- **iOS 17.0+** +- **Tuist 4.0+** +- **Swift 5.9+** + +## 📈 성능 최적화 + +- **모듈화**: Feature별 독립 빌드로 컴파일 시간 50% 단축 +- **이미지 캐싱**: 50MB 제한 NSCache로 메모리 효율성 확보 +- **데이터 캐싱**: SwiftData 기반 오프라인 지원 +- **네트워크 최적화**: Moya + Alamofire 기반 효율적 API 통신 + +## 🔮 향후 계획 + +- **통합 캐시 매니저**: 도메인별 캐시 서비스 통합 ## 📄 라이선스 -Copyright © 2025 SpartaCoding. All rights reserved. \ No newline at end of file +Copyright © 2025 SpartaCoding. All rights reserved. + +--- + +
+ +**🧳 함께하는 여행을 더 스마트하게, SseuDam과 함께하세요! ✨** + +
\ No newline at end of file