diff --git a/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml b/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml
index 78892e4..eb8b36a 100644
--- a/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml
+++ b/.github/workflows/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml
@@ -38,6 +38,11 @@ on:
repository_dispatch:
types: [build-android-app]
+permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+
# ============================================
# ๐ง ํ๋ก์ ํธ๋ณ ์ค์ (์๋ ๊ฐ๋ค์ ์์ ํ์ธ์)
# ============================================
diff --git a/.github/workflows/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml b/.github/workflows/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml
index 83f640f..d59b2f1 100644
--- a/.github/workflows/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml
+++ b/.github/workflows/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml
@@ -51,6 +51,11 @@ on:
repository_dispatch:
types: [build-ios-app]
+permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+
# ============================================
# ๐ง ํ๋ก์ ํธ๋ณ ์ค์ (์๋ ๊ฐ๋ค์ ์์ ํ์ธ์)
# ============================================
diff --git a/.gitignore b/.gitignore
index 839e51b..16c7970 100644
--- a/.gitignore
+++ b/.gitignore
@@ -80,3 +80,13 @@ ios/fastlane/*.p12
# Android CI/CD - ๋ฏผ๊ฐํ ํ์ผ (์๋ ์์ฑ๋จ)
android/fastlane/service-account.json
+# macOS CocoaPods - ์๋ ์์ฑ ํ์ผ
+macos/Pods/
+macos/Podfile.lock
+macos/.symlinks/
+macos/Flutter/ephemeral/
+
+# iOS CocoaPods - ์๋ ์์ฑ ํ์ผ
+ios/Pods/
+ios/.symlinks/
+ios/Flutter/ephemeral/
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 9ad3087..82ab00d 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -11,79 +11,79 @@ PODS:
- PromisesObjC (~> 2.4)
- device_info_plus (0.0.1):
- Flutter
- - Firebase/Auth (12.6.0):
+ - Firebase/Auth (12.8.0):
- Firebase/CoreOnly
- - FirebaseAuth (~> 12.6.0)
- - Firebase/CoreOnly (12.6.0):
- - FirebaseCore (~> 12.6.0)
- - Firebase/Crashlytics (12.6.0):
+ - FirebaseAuth (~> 12.8.0)
+ - Firebase/CoreOnly (12.8.0):
+ - FirebaseCore (~> 12.8.0)
+ - Firebase/Crashlytics (12.8.0):
- Firebase/CoreOnly
- - FirebaseCrashlytics (~> 12.6.0)
- - Firebase/Messaging (12.6.0):
+ - FirebaseCrashlytics (~> 12.8.0)
+ - Firebase/Messaging (12.8.0):
- Firebase/CoreOnly
- - FirebaseMessaging (~> 12.6.0)
- - firebase_auth (6.1.3):
- - Firebase/Auth (= 12.6.0)
+ - FirebaseMessaging (~> 12.8.0)
+ - firebase_auth (6.1.4):
+ - Firebase/Auth (= 12.8.0)
- firebase_core
- Flutter
- - firebase_core (4.3.0):
- - Firebase/CoreOnly (= 12.6.0)
+ - firebase_core (4.4.0):
+ - Firebase/CoreOnly (= 12.8.0)
- Flutter
- - firebase_crashlytics (5.0.6):
- - Firebase/Crashlytics (= 12.6.0)
+ - firebase_crashlytics (5.0.7):
+ - Firebase/Crashlytics (= 12.8.0)
- firebase_core
- Flutter
- - firebase_messaging (16.1.0):
- - Firebase/Messaging (= 12.6.0)
+ - firebase_messaging (16.1.1):
+ - Firebase/Messaging (= 12.8.0)
- firebase_core
- Flutter
- - FirebaseAppCheckInterop (12.6.0)
- - FirebaseAuth (12.6.0):
- - FirebaseAppCheckInterop (~> 12.6.0)
- - FirebaseAuthInterop (~> 12.6.0)
- - FirebaseCore (~> 12.6.0)
- - FirebaseCoreExtension (~> 12.6.0)
+ - FirebaseAppCheckInterop (12.8.0)
+ - FirebaseAuth (12.8.0):
+ - FirebaseAppCheckInterop (~> 12.8.0)
+ - FirebaseAuthInterop (~> 12.8.0)
+ - FirebaseCore (~> 12.8.0)
+ - FirebaseCoreExtension (~> 12.8.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GTMSessionFetcher/Core (< 6.0, >= 3.4)
- RecaptchaInterop (~> 101.0)
- - FirebaseAuthInterop (12.6.0)
- - FirebaseCore (12.6.0):
- - FirebaseCoreInternal (~> 12.6.0)
+ - FirebaseAuthInterop (12.8.0)
+ - FirebaseCore (12.8.0):
+ - FirebaseCoreInternal (~> 12.8.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- - FirebaseCoreExtension (12.6.0):
- - FirebaseCore (~> 12.6.0)
- - FirebaseCoreInternal (12.6.0):
+ - FirebaseCoreExtension (12.8.0):
+ - FirebaseCore (~> 12.8.0)
+ - FirebaseCoreInternal (12.8.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- - FirebaseCrashlytics (12.6.0):
- - FirebaseCore (~> 12.6.0)
- - FirebaseInstallations (~> 12.6.0)
- - FirebaseRemoteConfigInterop (~> 12.6.0)
- - FirebaseSessions (~> 12.6.0)
+ - FirebaseCrashlytics (12.8.0):
+ - FirebaseCore (~> 12.8.0)
+ - FirebaseInstallations (~> 12.8.0)
+ - FirebaseRemoteConfigInterop (~> 12.8.0)
+ - FirebaseSessions (~> 12.8.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- - FirebaseInstallations (12.6.0):
- - FirebaseCore (~> 12.6.0)
+ - FirebaseInstallations (12.8.0):
+ - FirebaseCore (~> 12.8.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- - FirebaseMessaging (12.6.0):
- - FirebaseCore (~> 12.6.0)
- - FirebaseInstallations (~> 12.6.0)
+ - FirebaseMessaging (12.8.0):
+ - FirebaseCore (~> 12.8.0)
+ - FirebaseInstallations (~> 12.8.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- - FirebaseRemoteConfigInterop (12.6.0)
- - FirebaseSessions (12.6.0):
- - FirebaseCore (~> 12.6.0)
- - FirebaseCoreExtension (~> 12.6.0)
- - FirebaseInstallations (~> 12.6.0)
+ - FirebaseRemoteConfigInterop (12.8.0)
+ - FirebaseSessions (12.8.0):
+ - FirebaseCore (~> 12.8.0)
+ - FirebaseCoreExtension (~> 12.8.0)
+ - FirebaseInstallations (~> 12.8.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
@@ -147,9 +147,6 @@ PODS:
- nanopb/encode (3.30910.0)
- package_info_plus (0.4.5):
- Flutter
- - path_provider_foundation (0.0.1):
- - Flutter
- - FlutterMacOS
- PromisesObjC (2.4.0)
- PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0)
@@ -171,7 +168,6 @@ DEPENDENCIES:
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
@@ -222,8 +218,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/google_sign_in_ios/darwin"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
- path_provider_foundation:
- :path: ".symlinks/plugins/path_provider_foundation/darwin"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sign_in_with_apple:
@@ -232,40 +226,39 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
- device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
- Firebase: a451a7b61536298fd5cbfe3a746fd40443a50679
- firebase_auth: c90f1b17fe53a1e9a63222d0b2f33256879b1a92
- firebase_core: 7ca5e04fc97329a2245376eba53c5bed113ca21e
- firebase_crashlytics: eb4e3214716bff15ccd3a554dd1cf7922115c430
- firebase_messaging: d9fd1aeeabefbbee02336521ccd19fc6e93c4625
- FirebaseAppCheckInterop: e2178171b4145013c7c1a3cc464d1d446d3a1896
- FirebaseAuth: 613c463cb43545a7fd2cd99ade09b78ac472c544
- FirebaseAuthInterop: db06756ef028006d034b6004dc0c37c24f7828d4
- FirebaseCore: 0e38ad5d62d980a47a64b8e9301ffa311457be04
- FirebaseCoreExtension: 032fd6f8509e591fda8cb76f6651f20d926b121f
- FirebaseCoreInternal: 69bf1306a05b8ac43004f6cc1f804bb7b05b229e
- FirebaseCrashlytics: 3d6248c50726ee7832aef0e53cb84c9e64d9fa7e
- FirebaseInstallations: 631b38da2e11a83daa4bfb482f79d286a5dfa7ad
- FirebaseMessaging: a61bc42dcab3f7a346d94bbb54dab2c9435b18b2
- FirebaseRemoteConfigInterop: 3443b8cb8fffd76bb3e03b2a84bfd3db952fcda4
- FirebaseSessions: 2e8f808347e665dff3e5843f275715f07045297d
+ device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
+ Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d
+ firebase_auth: e9031a1dbe04a90d98e8d11ff2302352a1c6d9e8
+ firebase_core: ee30637e6744af8e0c12a6a1e8a9718506ec2398
+ firebase_crashlytics: 28b8f39df8104131376393e6af658b8b77dd120f
+ firebase_messaging: 343de01a8d3e18b60df0c6d37f7174c44ae38e02
+ FirebaseAppCheckInterop: ba3dc604a89815379e61ec2365101608d365cf7d
+ FirebaseAuth: 4c289b1a43f5955283244a55cf6bd616de344be5
+ FirebaseAuthInterop: 95363fe96493cb4f106656666a0768b420cba090
+ FirebaseCore: 0dbad74bda10b8fb9ca34ad8f375fb9dd3ebef7c
+ FirebaseCoreExtension: 6605938d51f765d8b18bfcafd2085276a252bee2
+ FirebaseCoreInternal: fe5fa466aeb314787093a7dce9f0beeaad5a2a21
+ FirebaseCrashlytics: fb31c6907e5b52aa252668394d3f1ab326df1511
+ FirebaseInstallations: 6a14ab3d694ebd9f839c48d330da5547e9ca9dc0
+ FirebaseMessaging: 7f42cfd10ec64181db4e01b305a613791c8e782c
+ FirebaseRemoteConfigInterop: 869ddca16614f979e5c931ece11fbb0b8729ed41
+ FirebaseSessions: d614ca154c63dbbc6c10d6c38259c2162c4e7c9b
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
- flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
- flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
- google_sign_in_ios: 7411fab6948df90490dc4620ecbcabdc3ca04017
+ flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
+ flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
+ google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
- package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
- path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
+ package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba
- shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
- sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
+ shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
+ sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
PODFILE CHECKSUM: 85d318c08613be190fccc1abd43524ac3b83a41b
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 53f1136..2777e26 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -701,7 +701,7 @@
DEVELOPMENT_TEAM = 98QY4938R9;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
- GENERATE_INFOPLIST_FILE = YES;
+ GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = com.elipair.mapsy.share/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = com.elipair.mapsy.share;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
@@ -746,7 +746,7 @@
DEVELOPMENT_TEAM = 98QY4938R9;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
- GENERATE_INFOPLIST_FILE = YES;
+ GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = com.elipair.mapsy.share/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = com.elipair.mapsy.share;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
@@ -788,7 +788,7 @@
DEVELOPMENT_TEAM = 98QY4938R9;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
- GENERATE_INFOPLIST_FILE = YES;
+ GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = com.elipair.mapsy.share/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = com.elipair.mapsy.share;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
diff --git a/ios/com.elipair.mapsy.share/Info.plist b/ios/com.elipair.mapsy.share/Info.plist
index faa0de4..269a4c6 100644
--- a/ios/com.elipair.mapsy.share/Info.plist
+++ b/ios/com.elipair.mapsy.share/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
XPC!
CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
+ 1.0
CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
+ 1
NSExtension
NSExtensionAttributes
diff --git a/lib/core/constants/api_endpoints.dart b/lib/core/constants/api_endpoints.dart
index 7981576..dc345d7 100644
--- a/lib/core/constants/api_endpoints.dart
+++ b/lib/core/constants/api_endpoints.dart
@@ -3,59 +3,109 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
/// API ์๋ํฌ์ธํธ ์ค์ ๊ด๋ฆฌ
/// Centralized API Endpoint Management
///
-/// ํ๊ฒฝ ๋ณ์(.env)๋ฅผ ํตํด ์ค์ ๊ด๋ฆฌ
-/// Configuration managed via environment variables (.env)
+/// ํ๊ฒฝ ๋ณ์(.env)๋ฅผ ํตํด Base URL ์ค์ ๊ด๋ฆฌ
+/// Base URL configuration managed via environment variables (.env)
///
/// **์ฌ์ฉ ๊ฐ๋ฅํ ํ๊ฒฝ ๋ณ์**:
-/// - `API_BASE_URL`: ๋ฐฑ์๋ API Base URL
+/// - `API_BASE_URL`: ๋ฐฑ์๋ API Base URL (๊ธฐ๋ณธ๊ฐ: https://api.mapsy.suhsaechan.kr)
/// - `WS_URL`: WebSocket ์ฐ๊ฒฐ URL
/// - `USE_MOCK_API`: Mock API ์ฌ์ฉ ์ฌ๋ถ (true/false)
class ApiEndpoints {
// Private ์์ฑ์ - ์ธ์คํด์คํ ๋ฐฉ์ง
- // Private constructor to prevent instantiation
ApiEndpoints._();
// ============================================
// Base URL (ํ๊ฒฝ ๋ณ์์์ ๋ก๋)
- // Base URL (loaded from environment variables)
// ============================================
- /// API Base URL (.env์์ ๋ก๋)
- /// API Base URL (loaded from .env)
- ///
- /// **๊ธฐ๋ณธ๊ฐ**: `http://localhost:8080`
- /// **Default**: `http://localhost:8080`
+ /// API Base URL (.env์์ ๋ก๋, ๊ธฐ๋ณธ๊ฐ: ๊ฐ๋ฐ ์๋ฒ)
static String get baseUrl =>
- dotenv.env['API_BASE_URL'] ?? 'http://localhost:8080';
+ dotenv.env['API_BASE_URL'] ?? 'https://api.mapsy.suhsaechan.kr';
/// WebSocket URL (.env์์ ๋ก๋)
- /// WebSocket URL (loaded from .env)
- ///
- /// **๊ธฐ๋ณธ๊ฐ**: `ws://localhost:8080/ws`
- /// **Default**: `ws://localhost:8080/ws`
- static String get wsUrl => dotenv.env['WS_URL'] ?? 'ws://localhost:8080/ws';
+ static String get wsUrl =>
+ dotenv.env['WS_URL'] ?? 'wss://api.mapsy.suhsaechan.kr/ws';
/// Mock API ์ฌ์ฉ ์ฌ๋ถ (.env์์ ๋ก๋)
- /// Whether to use Mock API (loaded from .env)
- ///
- /// **๊ธฐ๋ณธ๊ฐ**: `true`
- /// **Default**: `true`
static bool get useMockApi =>
dotenv.env['USE_MOCK_API']?.toLowerCase() == 'true';
// ============================================
- // ์ฐธ๊ณ : ๊ตฌ์ฒด์ ์ธ API ์๋ํฌ์ธํธ
- // Note: Specific API Endpoints
- // ============================================
- //
- // ๋ฐฑ์๋ API๊ฐ ์ ์๋๋ฉด ์๋์ ๊ฐ์ ํํ๋ก ์ถ๊ฐ ์์ :
- // Will be added in the following format once backend API is defined:
- //
- // static const String googleLogin = '/auth/google';
- // static const String refreshToken = '/auth/refresh';
- // static const String createSession = '/api/sessions';
- // static String getSession(String sessionId) => '/api/sessions/$sessionId';
- //
- // ํ์ฌ๋ ๋ฐฑ์๋ ์ ์ ์ ์ด๋ฏ๋ก ๋น ์ํ๋ก ์ ์ง
- // Currently kept empty until backend is defined
+ // Auth API Endpoints
+ // ============================================
+
+ /// ์์
๋ก๊ทธ์ธ (Firebase ID Token โ ๋ฐฑ์๋ JWT)
+ static const String signIn = '/api/auth/sign-in';
+
+ /// ํ ํฐ ์ฌ๋ฐ๊ธ (Refresh Token โ ์ Access Token)
+ static const String reissue = '/api/auth/reissue';
+
+ /// ๋ก๊ทธ์์
+ static const String logout = '/api/auth/logout';
+
+ /// ํ์ ํํด
+ static const String withdraw = '/api/auth/withdraw';
+
+ // ============================================
+ // Member API Endpoints
+ // ============================================
+
+ /// ํ์ ๊ธฐ๋ณธ ๊ฒฝ๋ก
+ static const String members = '/api/members';
+
+ /// ์จ๋ณด๋ฉ: ์ฝ๊ด ๋์
+ static const String onboardingTerms = '/api/members/onboarding/terms';
+
+ /// ์จ๋ณด๋ฉ: ์๋
์์ผ
+ static const String onboardingBirthDate = '/api/members/onboarding/birth-date';
+
+ /// ์จ๋ณด๋ฉ: ์ฑ๋ณ
+ static const String onboardingGender = '/api/members/onboarding/gender';
+
+ /// ํ๋กํ ์
๋ฐ์ดํธ (๋๋ค์ ๋ฑ)
+ static const String memberProfile = '/api/members/profile';
+
+ /// ๋๋ค์ ์ค๋ณต ํ์ธ
+ static const String checkName = '/api/members/check-name';
+
+ // ============================================
+ // Place API Endpoints
+ // ============================================
+
+ /// ์ฅ์ ๊ธฐ๋ณธ ๊ฒฝ๋ก
+ static const String place = '/api/place';
+
+ /// ์ฅ์ ์์ธ ์กฐํ
+ static String placeDetail(String placeId) => '/api/place/$placeId';
+
+ /// ์์ ์ ์ฅ ์ฅ์ ๋ชฉ๋ก
+ static const String temporaryPlaces = '/api/place/temporary';
+
+ /// ์ ์ฅ๋ ์ฅ์ ๋ชฉ๋ก
+ static const String savedPlaces = '/api/place/saved';
+
+ /// ์ฅ์ ์ ์ฅ
+ static String savePlace(String placeId) => '/api/place/$placeId/save';
+
+ // ============================================
+ // Content API Endpoints (AI ์ถ์ถ)
+ // ============================================
+
+ /// AI ๋ถ์ ์์ฒญ (SNS URL โ ์ฅ์ ์ถ์ถ)
+ static const String contentAnalyze = '/api/content/analyze';
+
+ /// ์ฝํ
์ธ ๊ธฐ๋ณธ ๊ฒฝ๋ก
+ static const String content = '/api/content';
+
+ /// ์ฝํ
์ธ ์์ธ ์กฐํ (ํด๋ง์ฉ)
+ static String contentDetail(String contentId) => '/api/content/$contentId';
+
+ /// ํ์ ์ฝํ
์ธ ๋ชฉ๋ก
+ static const String memberContent = '/api/content/member';
+
+ /// ์ต๊ทผ ์ฝํ
์ธ
+ static const String recentContent = '/api/content/recent';
+
+ /// ์ ์ฅ๋ ์ฅ์ (์ฝํ
์ธ ์์)
+ static const String contentSavedPlaces = '/api/content/place/saved';
}
diff --git a/lib/core/constants/app_colors.dart b/lib/core/constants/app_colors.dart
index 822032e..f05f595 100644
--- a/lib/core/constants/app_colors.dart
+++ b/lib/core/constants/app_colors.dart
@@ -34,6 +34,40 @@ class AppColors {
/// Robber team color
static const Color robberTeam = Color(0xFFE53935);
+ // ============================================
+ // Gray ์ค์ผ์ผ ์์ (Gray Scale Colors)
+ // ============================================
+
+ /// Gray 50 - ๊ฐ์ฅ ๋ฐ์ ํ์
+ static const Color gray50 = Color(0xFFFAFAFA);
+
+ /// Gray 100
+ static const Color gray100 = Color(0xFFF5F5F5);
+
+ /// Gray 200
+ static const Color gray200 = Color(0xFFEEEEEE);
+
+ /// Gray 300
+ static const Color gray300 = Color(0xFFE0E0E0);
+
+ /// Gray 400
+ static const Color gray400 = Color(0xFFBDBDBD);
+
+ /// Gray 500
+ static const Color gray500 = Color(0xFF9E9E9E);
+
+ /// Gray 600
+ static const Color gray600 = Color(0xFF757575);
+
+ /// Gray 700
+ static const Color gray700 = Color(0xFF616161);
+
+ /// Gray 800
+ static const Color gray800 = Color(0xFF424242);
+
+ /// Gray 900 - ๊ฐ์ฅ ์ด๋์ด ํ์
+ static const Color gray900 = Color(0xFF212121);
+
// ============================================
// ๋ฐฐ๊ฒฝ ์์ (Background Colors)
// ============================================
diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart
new file mode 100644
index 0000000..edcfc40
--- /dev/null
+++ b/lib/core/network/api_client.dart
@@ -0,0 +1,84 @@
+import 'package:dio/dio.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../constants/api_endpoints.dart';
+import 'auth_interceptor.dart';
+import 'error_interceptor.dart';
+import 'token_refresh_interceptor.dart';
+
+part 'api_client.g.dart';
+
+/// Dio ์ธ์คํด์ค Provider
+///
+/// ์ฑ ์ ์ฒด์์ ์ฌ์ฉํ๋ HTTP ํด๋ผ์ด์ธํธ์
๋๋ค.
+/// ์ธ์ฆ, ํ ํฐ ๊ฐฑ์ , ์๋ฌ ๋ณํ ์ธํฐ์
ํฐ๊ฐ ์๋์ผ๋ก ์ ์ฉ๋ฉ๋๋ค.
+///
+/// **์ฌ์ฉ๋ฒ**:
+/// ```dart
+/// final dio = ref.watch(dioProvider);
+/// final response = await dio.get('/api/some-endpoint');
+/// ```
+@riverpod
+Dio dio(Ref ref) {
+ final dio = Dio(
+ BaseOptions(
+ baseUrl: ApiEndpoints.baseUrl,
+ connectTimeout: const Duration(seconds: 30),
+ receiveTimeout: const Duration(minutes: 30), // LLM ์ฒ๋ฆฌ ์๊ฐ ๊ณ ๋ ค
+ sendTimeout: const Duration(minutes: 30), // LLM ์ฒ๋ฆฌ ์๊ฐ ๊ณ ๋ ค
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ },
+ ),
+ );
+
+ // ์ธํฐ์
ํฐ ์ถ๊ฐ (์์ ์ค์!)
+ // 1. ๋ก๊น
(๊ฐ๋ฐ ๋ชจ๋์์๋ง)
+ if (kDebugMode) {
+ dio.interceptors.add(
+ LogInterceptor(
+ requestHeader: true,
+ requestBody: true,
+ responseHeader: false,
+ responseBody: true,
+ error: true,
+ logPrint: (obj) => debugPrint('๐ก $obj'),
+ ),
+ );
+ }
+
+ // 2. ์ธ์ฆ (Access Token ์ฃผ์
)
+ dio.interceptors.add(AuthInterceptor(ref));
+
+ // 3. ํ ํฐ ๊ฐฑ์ (401 โ Refresh โ ์ฌ์๋)
+ // refreshDio๋ฅผ ๋ณ๋๋ก ์ฌ์ฉํ์ฌ ์ธํฐ์
ํฐ ์ํ ๋ฐฉ์ง
+ final refreshDio = ref.read(refreshDioProvider);
+ dio.interceptors.add(TokenRefreshInterceptor(ref, dio, refreshDio));
+
+ // 4. ์๋ฌ ๋ณํ (DioException โ AppException)
+ dio.interceptors.add(ErrorInterceptor());
+
+ return dio;
+}
+
+/// ํ ํฐ ๊ฐฑ์ ์ ์ฉ Dio ์ธ์คํด์ค
+///
+/// TokenRefreshInterceptor์์ ์ฌ์ฉํ๋ ๋ณ๋์ Dio ์ธ์คํด์ค์
๋๋ค.
+/// ์ธํฐ์
ํฐ ์ํ์ ๋ฐฉ์งํ๊ธฐ ์ํด ์ต์ํ์ ์ค์ ๋ง ์ ์ฉํฉ๋๋ค.
+@riverpod
+Dio refreshDio(Ref ref) {
+ return Dio(
+ BaseOptions(
+ baseUrl: ApiEndpoints.baseUrl,
+ connectTimeout: const Duration(seconds: 10),
+ receiveTimeout: const Duration(seconds: 10),
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ },
+ ),
+ );
+}
diff --git a/lib/core/network/api_client.g.dart b/lib/core/network/api_client.g.dart
new file mode 100644
index 0000000..750d308
--- /dev/null
+++ b/lib/core/network/api_client.g.dart
@@ -0,0 +1,60 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'api_client.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$dioHash() => r'59e14cd015af5ec5163321683e8232da6291ca1b';
+
+/// Dio ์ธ์คํด์ค Provider
+///
+/// ์ฑ ์ ์ฒด์์ ์ฌ์ฉํ๋ HTTP ํด๋ผ์ด์ธํธ์
๋๋ค.
+/// ์ธ์ฆ, ํ ํฐ ๊ฐฑ์ , ์๋ฌ ๋ณํ ์ธํฐ์
ํฐ๊ฐ ์๋์ผ๋ก ์ ์ฉ๋ฉ๋๋ค.
+///
+/// **์ฌ์ฉ๋ฒ**:
+/// ```dart
+/// final dio = ref.watch(dioProvider);
+/// final response = await dio.get('/api/some-endpoint');
+/// ```
+///
+/// Copied from [dio].
+@ProviderFor(dio)
+final dioProvider = AutoDisposeProvider.internal(
+ dio,
+ name: r'dioProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$dioHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+@Deprecated('Will be removed in 3.0. Use Ref instead')
+// ignore: unused_element
+typedef DioRef = AutoDisposeProviderRef;
+String _$refreshDioHash() => r'4e4a3a496c4b824d722ceb8ea9ed618351f0e92c';
+
+/// ํ ํฐ ๊ฐฑ์ ์ ์ฉ Dio ์ธ์คํด์ค
+///
+/// TokenRefreshInterceptor์์ ์ฌ์ฉํ๋ ๋ณ๋์ Dio ์ธ์คํด์ค์
๋๋ค.
+/// ์ธํฐ์
ํฐ ์ํ์ ๋ฐฉ์งํ๊ธฐ ์ํด ์ต์ํ์ ์ค์ ๋ง ์ ์ฉํฉ๋๋ค.
+///
+/// Copied from [refreshDio].
+@ProviderFor(refreshDio)
+final refreshDioProvider = AutoDisposeProvider.internal(
+ refreshDio,
+ name: r'refreshDioProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$refreshDioHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+@Deprecated('Will be removed in 3.0. Use Ref instead')
+// ignore: unused_element
+typedef RefreshDioRef = AutoDisposeProviderRef;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
diff --git a/lib/core/network/auth_interceptor.dart b/lib/core/network/auth_interceptor.dart
new file mode 100644
index 0000000..d2a83cc
--- /dev/null
+++ b/lib/core/network/auth_interceptor.dart
@@ -0,0 +1,57 @@
+import 'package:dio/dio.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'token_storage.dart';
+
+/// ์ธ์ฆ ์ธํฐ์
ํฐ
+///
+/// ๋ชจ๋ HTTP ์์ฒญ์ Access Token์ ์๋์ผ๋ก ์ฃผ์
ํฉ๋๋ค.
+///
+/// **๋์**:
+/// 1. ์์ฒญ ์ : Authorization ํค๋์ Bearer Token ์ถ๊ฐ
+/// 2. ์ธ์ฆ์ด ํ์ ์๋ ๊ฒฝ๋ก๋ ์ ์ธ (์: /auth/sign-in)
+class AuthInterceptor extends Interceptor {
+ final Ref _ref;
+
+ /// ์ธ์ฆ์ด ํ์ ์๋ ๊ฒฝ๋ก ๋ชฉ๋ก
+ static const _publicPaths = [
+ '/api/auth/sign-in',
+ '/api/auth/reissue',
+ ];
+
+ AuthInterceptor(this._ref);
+
+ @override
+ Future onRequest(
+ RequestOptions options,
+ RequestInterceptorHandler handler,
+ ) async {
+ // ์ธ์ฆ์ด ํ์ ์๋ ๊ฒฝ๋ก๋ ํ ํฐ ์ฃผ์
์๋ต
+ if (_isPublicPath(options.path)) {
+ debugPrint('๐ Public path, skipping auth: ${options.path}');
+ return handler.next(options);
+ }
+
+ try {
+ final tokenStorage = _ref.read(tokenStorageProvider);
+ final accessToken = await tokenStorage.getAccessToken();
+
+ if (accessToken != null && accessToken.isNotEmpty) {
+ options.headers['Authorization'] = 'Bearer $accessToken';
+ debugPrint('๐ Token injected for: ${options.path}');
+ } else {
+ debugPrint('โ ๏ธ No access token available for: ${options.path}');
+ }
+ } catch (e) {
+ debugPrint('โ Error reading token: $e');
+ }
+
+ return handler.next(options);
+ }
+
+ /// ์ธ์ฆ์ด ํ์ ์๋ ๊ฒฝ๋ก์ธ์ง ํ์ธ
+ bool _isPublicPath(String path) {
+ return _publicPaths.any((publicPath) => path.contains(publicPath));
+ }
+}
diff --git a/lib/core/network/error_interceptor.dart b/lib/core/network/error_interceptor.dart
new file mode 100644
index 0000000..61efdfe
--- /dev/null
+++ b/lib/core/network/error_interceptor.dart
@@ -0,0 +1,141 @@
+import 'dart:io';
+
+import 'package:dio/dio.dart';
+import 'package:flutter/foundation.dart';
+
+import '../errors/app_exception.dart';
+
+/// ์๋ฌ ์ธํฐ์
ํฐ
+///
+/// Dio ์๋ฌ๋ฅผ ์ฑ์ ์ปค์คํ
์์ธ(AppException)๋ก ๋ณํํฉ๋๋ค.
+///
+/// **๋ณํ ๊ท์น**:
+/// - ๋คํธ์ํฌ ์ฐ๊ฒฐ ์๋ฌ โ NetworkException
+/// - 401 Unauthorized โ AuthException
+/// - 400 Bad Request โ ValidationException
+/// - 500+ ์๋ฒ ์๋ฌ โ ServerException
+/// - ๊ทธ ์ธ โ NetworkException
+class ErrorInterceptor extends Interceptor {
+ @override
+ void onError(DioException err, ErrorInterceptorHandler handler) {
+ final exception = _convertToAppException(err);
+
+ debugPrint('โ API Error: ${exception.toString()}');
+ debugPrint(' Path: ${err.requestOptions.path}');
+ debugPrint(' Status: ${err.response?.statusCode}');
+
+ // DioException์ ์ ์งํ๋, error ํ๋์ AppException์ ๋ด์์ ์ ๋ฌ
+ handler.next(DioException(
+ requestOptions: err.requestOptions,
+ response: err.response,
+ type: err.type,
+ error: exception,
+ message: exception.message,
+ ));
+ }
+
+ /// DioException์ AppException์ผ๋ก ๋ณํ
+ AppException _convertToAppException(DioException err) {
+ // ๋คํธ์ํฌ ์ฐ๊ฒฐ ์๋ฌ
+ if (err.type == DioExceptionType.connectionError ||
+ err.type == DioExceptionType.connectionTimeout ||
+ err.error is SocketException) {
+ return NetworkException(
+ message: '์ธํฐ๋ท ์ฐ๊ฒฐ์ ํ์ธํด์ฃผ์ธ์.',
+ code: 'NETWORK_ERROR',
+ originalException: err,
+ );
+ }
+
+ // ์์ฒญ ํ์์์
+ if (err.type == DioExceptionType.sendTimeout ||
+ err.type == DioExceptionType.receiveTimeout) {
+ return NetworkException(
+ message: '์๋ฒ ์๋ต์ด ๋๋ฌด ๋๋ฆฝ๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.',
+ code: 'TIMEOUT',
+ originalException: err,
+ );
+ }
+
+ // HTTP ์ํ ์ฝ๋ ๊ธฐ๋ฐ ์๋ฌ
+ final statusCode = err.response?.statusCode;
+ final responseData = err.response?.data;
+
+ // ์๋ฒ์์ ์ ๋ฌํ ์๋ฌ ๋ฉ์์ง ์ถ์ถ
+ String? serverMessage;
+ String? serverCode;
+ if (responseData is Map) {
+ serverMessage = responseData['message'] as String? ??
+ responseData['error'] as String?;
+ serverCode = responseData['code'] as String?;
+ }
+
+ switch (statusCode) {
+ case 400:
+ return ValidationException(
+ message: serverMessage ?? '์๋ชป๋ ์์ฒญ์
๋๋ค.',
+ code: serverCode ?? 'BAD_REQUEST',
+ originalException: err,
+ );
+
+ case 401:
+ return AuthException(
+ message: serverMessage ?? '์ธ์ฆ์ด ๋ง๋ฃ๋์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.',
+ code: serverCode ?? 'UNAUTHORIZED',
+ originalException: err,
+ );
+
+ case 403:
+ return AuthException(
+ message: serverMessage ?? '์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค.',
+ code: serverCode ?? 'FORBIDDEN',
+ originalException: err,
+ );
+
+ case 404:
+ return ServerException(
+ message: serverMessage ?? '์์ฒญํ ๋ฆฌ์์ค๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.',
+ code: serverCode ?? 'NOT_FOUND',
+ originalException: err,
+ );
+
+ case 409:
+ return ValidationException(
+ message: serverMessage ?? '์ค๋ณต๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.',
+ code: serverCode ?? 'CONFLICT',
+ originalException: err,
+ );
+
+ case 422:
+ return ValidationException(
+ message: serverMessage ?? '์
๋ ฅ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.',
+ code: serverCode ?? 'UNPROCESSABLE_ENTITY',
+ originalException: err,
+ );
+
+ case 429:
+ return NetworkException(
+ message: '์์ฒญ์ด ๋๋ฌด ๋ง์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.',
+ code: 'TOO_MANY_REQUESTS',
+ originalException: err,
+ );
+
+ case 500:
+ case 502:
+ case 503:
+ case 504:
+ return ServerException(
+ message: serverMessage ?? '์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.',
+ code: serverCode ?? 'SERVER_ERROR',
+ originalException: err,
+ );
+
+ default:
+ return NetworkException(
+ message: serverMessage ?? '์ ์ ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.',
+ code: serverCode ?? 'UNKNOWN_ERROR',
+ originalException: err,
+ );
+ }
+ }
+}
diff --git a/lib/core/network/token_refresh_interceptor.dart b/lib/core/network/token_refresh_interceptor.dart
new file mode 100644
index 0000000..ee1edf0
--- /dev/null
+++ b/lib/core/network/token_refresh_interceptor.dart
@@ -0,0 +1,175 @@
+import 'dart:async';
+
+import 'package:dio/dio.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import '../constants/api_endpoints.dart';
+import 'token_storage.dart';
+
+/// ํ ํฐ ๊ฐฑ์ ์ธํฐ์
ํฐ
+///
+/// 401 Unauthorized ์๋ต ์ Refresh Token์ผ๋ก Access Token์ ๊ฐฑ์ ํ๊ณ
+/// ์๋ ์์ฒญ์ ์ฌ์๋ํฉ๋๋ค.
+///
+/// **๋์**:
+/// 1. 401 ์๋ฌ ๊ฐ์ง
+/// 2. Refresh Token์ผ๋ก ์ Access Token ์์ฒญ (refreshDio ์ฌ์ฉ)
+/// 3. ์ ํ ํฐ ์ ์ฅ
+/// 4. ์๋ ์์ฒญ ์ฌ์๋
+/// 5. Refresh Token๋ ๋ง๋ฃ๋ ๊ฒฝ์ฐ โ ๋ก๊ทธ์์ ์ฒ๋ฆฌ
+///
+/// **์ฃผ์**: ํ ํฐ ๊ฐฑ์ ์์ฒญ ์ [_refreshDio]๋ฅผ ์ฌ์ฉํ์ฌ
+/// ์ธํฐ์
ํฐ ์ํ ํธ์ถ์ ๋ฐฉ์งํฉ๋๋ค.
+class TokenRefreshInterceptor extends Interceptor {
+ final Ref _ref;
+ final Dio _mainDio;
+ final Dio _refreshDio;
+
+ /// ํ ํฐ ๊ฐฑ์ ์ค ์ฌ๋ถ (์ค๋ณต ๊ฐฑ์ ๋ฐฉ์ง)
+ bool _isRefreshing = false;
+
+ /// ๊ฐฑ์ ์๋ฃ ๋๊ธฐ ์ค์ธ ์์ฒญ๋ค
+ final List<({RequestOptions options, ErrorInterceptorHandler handler})>
+ _pendingRequests = [];
+
+ TokenRefreshInterceptor(this._ref, this._mainDio, this._refreshDio);
+
+ @override
+ Future onError(
+ DioException err,
+ ErrorInterceptorHandler handler,
+ ) async {
+ // 401 ์๋ฌ๊ฐ ์๋๋ฉด ๊ทธ๋๋ก ์ ๋ฌ
+ if (err.response?.statusCode != 401) {
+ return handler.next(err);
+ }
+
+ // ํ ํฐ ๊ฐฑ์ ์์ฒญ ์์ฒด๊ฐ ์คํจํ ๊ฒฝ์ฐ โ ๋ก๊ทธ์์
+ if (err.requestOptions.path.contains(ApiEndpoints.reissue)) {
+ debugPrint('๐ด Refresh token expired, logging out...');
+ await _handleLogout();
+ return handler.next(err);
+ }
+
+ debugPrint('๐ 401 detected, attempting token refresh...');
+
+ // ์ด๋ฏธ ๊ฐฑ์ ์ค์ด๋ฉด ๋๊ธฐ์ด์ ์ถ๊ฐ
+ if (_isRefreshing) {
+ debugPrint('โณ Token refresh in progress, queuing request...');
+ _pendingRequests.add((options: err.requestOptions, handler: handler));
+ return;
+ }
+
+ _isRefreshing = true;
+
+ try {
+ // ํ ํฐ ๊ฐฑ์ ์๋
+ final success = await _refreshToken();
+
+ if (success) {
+ debugPrint('โ
Token refreshed successfully');
+
+ // ์๋ ์์ฒญ ์ฌ์๋
+ final response = await _retryRequest(err.requestOptions);
+ handler.resolve(response);
+
+ // ๋๊ธฐ ์ค์ธ ์์ฒญ๋ค๋ ์ฌ์๋
+ _retryPendingRequests();
+ } else {
+ debugPrint('๐ด Token refresh failed, logging out...');
+ await _handleLogout();
+ handler.next(err);
+ _rejectPendingRequests(err);
+ }
+ } catch (e) {
+ debugPrint('โ Token refresh error: $e');
+ await _handleLogout();
+ handler.next(err);
+ _rejectPendingRequests(err);
+ } finally {
+ _isRefreshing = false;
+ }
+ }
+
+ /// Refresh Token์ผ๋ก ์ Access Token ์์ฒญ
+ Future _refreshToken() async {
+ try {
+ final tokenStorage = _ref.read(tokenStorageProvider);
+ final refreshToken = await tokenStorage.getRefreshToken();
+
+ if (refreshToken == null || refreshToken.isEmpty) {
+ debugPrint('โ ๏ธ No refresh token available');
+ return false;
+ }
+
+ // ํ ํฐ ๊ฐฑ์ ์์ฒญ (์ธํฐ์
ํฐ ์ํ ๋ฐฉ์ง๋ฅผ ์ํด ๋ณ๋ Dio ์ธ์คํด์ค ์ฌ์ฉ)
+ final response = await _refreshDio.post(
+ ApiEndpoints.reissue,
+ data: {'refreshToken': refreshToken},
+ );
+
+ if (response.statusCode == 200 && response.data != null) {
+ final newAccessToken = response.data['accessToken'] as String?;
+ final newRefreshToken = response.data['refreshToken'] as String?;
+
+ if (newAccessToken != null && newRefreshToken != null) {
+ await tokenStorage.saveTokens(
+ accessToken: newAccessToken,
+ refreshToken: newRefreshToken,
+ );
+ return true;
+ }
+ }
+
+ return false;
+ } catch (e) {
+ debugPrint('โ Refresh token request failed: $e');
+ return false;
+ }
+ }
+
+ /// ์๋ ์์ฒญ ์ฌ์๋
+ Future _retryRequest(RequestOptions requestOptions) async {
+ final tokenStorage = _ref.read(tokenStorageProvider);
+ final newAccessToken = await tokenStorage.getAccessToken();
+
+ // ์ ํ ํฐ์ผ๋ก ํค๋ ์
๋ฐ์ดํธ
+ requestOptions.headers['Authorization'] = 'Bearer $newAccessToken';
+
+ return await _mainDio.fetch(requestOptions);
+ }
+
+ /// ๋๊ธฐ ์ค์ธ ์์ฒญ๋ค ์ฌ์๋
+ void _retryPendingRequests() {
+ for (final pending in _pendingRequests) {
+ _retryRequest(pending.options).then(
+ (response) => pending.handler.resolve(response),
+ onError: (error) => pending.handler.reject(error as DioException),
+ );
+ }
+ _pendingRequests.clear();
+ }
+
+ /// ๋๊ธฐ ์ค์ธ ์์ฒญ๋ค ๊ฑฐ๋ถ
+ void _rejectPendingRequests(DioException err) {
+ for (final pending in _pendingRequests) {
+ pending.handler.next(err);
+ }
+ _pendingRequests.clear();
+ }
+
+ /// ๋ก๊ทธ์์ ์ฒ๋ฆฌ
+ Future _handleLogout() async {
+ try {
+ final tokenStorage = _ref.read(tokenStorageProvider);
+ await tokenStorage.clearTokens();
+ debugPrint('๐ช Tokens cleared, user logged out');
+
+ // TODO: ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ (GoRouter ์ฌ์ฉ)
+ // ref.read(routerProvider).go('/login');
+ } catch (e) {
+ debugPrint('โ Logout cleanup error: $e');
+ }
+ }
+}
diff --git a/lib/core/network/token_storage.dart b/lib/core/network/token_storage.dart
new file mode 100644
index 0000000..ac5a97d
--- /dev/null
+++ b/lib/core/network/token_storage.dart
@@ -0,0 +1,148 @@
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'token_storage.g.dart';
+
+/// ํ ํฐ ์ ์ฅ์ Provider
+@riverpod
+TokenStorage tokenStorage(Ref ref) {
+ return TokenStorage();
+}
+
+/// ํ ํฐ ๋ณด์ ์ ์ฅ์
+///
+/// flutter_secure_storage๋ฅผ ์ฌ์ฉํ์ฌ Access Token๊ณผ Refresh Token์
+/// ์์ ํ๊ฒ ์ ์ฅํ๊ณ ๊ด๋ฆฌํฉ๋๋ค.
+///
+/// **์ ์ฅ ํค**:
+/// - `access_token`: ๋ฐฑ์๋ API ์ธ์ฆ์ฉ JWT Access Token
+/// - `refresh_token`: Access Token ๊ฐฑ์ ์ฉ Refresh Token
+/// - `requires_onboarding`: ์จ๋ณด๋ฉ ํ์ ์ฌ๋ถ (true/false)
+/// - `onboarding_step`: ํ์ฌ ์จ๋ณด๋ฉ ๋จ๊ณ (TERMS, BIRTH_DATE, GENDER, NICKNAME)
+class TokenStorage {
+ static const _accessTokenKey = 'access_token';
+ static const _refreshTokenKey = 'refresh_token';
+ static const _requiresOnboardingKey = 'requires_onboarding';
+ static const _onboardingStepKey = 'onboarding_step';
+
+ final FlutterSecureStorage _storage;
+
+ TokenStorage({FlutterSecureStorage? storage})
+ : _storage = storage ??
+ const FlutterSecureStorage(
+ aOptions: AndroidOptions(encryptedSharedPreferences: true),
+ iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
+ );
+
+ // ============================================
+ // Access Token
+ // ============================================
+
+ /// Access Token ์ ์ฅ
+ Future saveAccessToken(String token) async {
+ await _storage.write(key: _accessTokenKey, value: token);
+ }
+
+ /// Access Token ์กฐํ
+ Future getAccessToken() async {
+ return await _storage.read(key: _accessTokenKey);
+ }
+
+ /// Access Token ์ญ์
+ Future deleteAccessToken() async {
+ await _storage.delete(key: _accessTokenKey);
+ }
+
+ // ============================================
+ // Refresh Token
+ // ============================================
+
+ /// Refresh Token ์ ์ฅ
+ Future saveRefreshToken(String token) async {
+ await _storage.write(key: _refreshTokenKey, value: token);
+ }
+
+ /// Refresh Token ์กฐํ
+ Future getRefreshToken() async {
+ return await _storage.read(key: _refreshTokenKey);
+ }
+
+ /// Refresh Token ์ญ์
+ Future deleteRefreshToken() async {
+ await _storage.delete(key: _refreshTokenKey);
+ }
+
+ // ============================================
+ // Token Pair (Access + Refresh)
+ // ============================================
+
+ /// Access Token๊ณผ Refresh Token์ ํจ๊ป ์ ์ฅ
+ Future saveTokens({
+ required String accessToken,
+ required String refreshToken,
+ }) async {
+ await Future.wait([
+ saveAccessToken(accessToken),
+ saveRefreshToken(refreshToken),
+ ]);
+ }
+
+ /// ๋ชจ๋ ํ ํฐ ์ญ์ (๋ก๊ทธ์์ ์)
+ Future clearTokens() async {
+ await Future.wait([
+ deleteAccessToken(),
+ deleteRefreshToken(),
+ deleteOnboardingState(),
+ ]);
+ }
+
+ /// ํ ํฐ ์กด์ฌ ์ฌ๋ถ ํ์ธ
+ Future hasTokens() async {
+ final accessToken = await getAccessToken();
+ return accessToken != null && accessToken.isNotEmpty;
+ }
+
+ // ============================================
+ // Onboarding State
+ // ============================================
+
+ /// ์จ๋ณด๋ฉ ์ํ ์ ์ฅ
+ Future saveOnboardingState({
+ required bool requiresOnboarding,
+ String? onboardingStep,
+ }) async {
+ await _storage.write(
+ key: _requiresOnboardingKey,
+ value: requiresOnboarding.toString(),
+ );
+ if (onboardingStep != null) {
+ await _storage.write(key: _onboardingStepKey, value: onboardingStep);
+ }
+ }
+
+ /// ์จ๋ณด๋ฉ ํ์ ์ฌ๋ถ ์กฐํ
+ Future getRequiresOnboarding() async {
+ final value = await _storage.read(key: _requiresOnboardingKey);
+ return value?.toLowerCase() == 'true';
+ }
+
+ /// ํ์ฌ ์จ๋ณด๋ฉ ๋จ๊ณ ์กฐํ
+ Future getOnboardingStep() async {
+ return await _storage.read(key: _onboardingStepKey);
+ }
+
+ /// ์จ๋ณด๋ฉ ์๋ฃ ์ฒ๋ฆฌ
+ Future completeOnboarding() async {
+ await _storage.write(key: _requiresOnboardingKey, value: 'false');
+ await _storage.delete(key: _onboardingStepKey);
+ }
+
+ /// ์จ๋ณด๋ฉ ์ํ ์ญ์
+ Future deleteOnboardingState() async {
+ await Future.wait([
+ _storage.delete(key: _requiresOnboardingKey),
+ _storage.delete(key: _onboardingStepKey),
+ ]);
+ }
+}
diff --git a/lib/core/network/token_storage.g.dart b/lib/core/network/token_storage.g.dart
new file mode 100644
index 0000000..6c468ef
--- /dev/null
+++ b/lib/core/network/token_storage.g.dart
@@ -0,0 +1,29 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'token_storage.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$tokenStorageHash() => r'd76686f5c7fe18426f0a40b20f703a6a382a2a30';
+
+/// ํ ํฐ ์ ์ฅ์ Provider
+///
+/// Copied from [tokenStorage].
+@ProviderFor(tokenStorage)
+final tokenStorageProvider = AutoDisposeProvider.internal(
+ tokenStorage,
+ name: r'tokenStorageProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$tokenStorageHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+@Deprecated('Will be removed in 3.0. Use Ref instead')
+// ignore: unused_element
+typedef TokenStorageRef = AutoDisposeProviderRef;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
diff --git a/lib/core/services/device/device_info_service.dart b/lib/core/services/device/device_info_service.dart
index c09afeb..31c3036 100644
--- a/lib/core/services/device/device_info_service.dart
+++ b/lib/core/services/device/device_info_service.dart
@@ -1,6 +1,40 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import 'device_id_manager.dart';
+
+part 'device_info_service.g.dart';
+
+/// DeviceInfoService Provider
+@riverpod
+DeviceInfoService deviceInfoService(Ref ref) {
+ return DeviceInfoService();
+}
+
+/// ๊ธฐ๊ธฐ ์ ๋ณด ๋ฐ์ดํฐ ํด๋์ค
+class DeviceInfo {
+ final String deviceName;
+ final String deviceType;
+ final String deviceId;
+ final String osVersion;
+ final bool isPhysicalDevice;
+
+ const DeviceInfo({
+ required this.deviceName,
+ required this.deviceType,
+ required this.deviceId,
+ required this.osVersion,
+ required this.isPhysicalDevice,
+ });
+
+ @override
+ String toString() {
+ return 'DeviceInfo(name: $deviceName, type: $deviceType, id: $deviceId, os: $osVersion, physical: $isPhysicalDevice)';
+ }
+}
/// ๊ธฐ๊ธฐ ์ ๋ณด ์์ง ์๋น์ค
///
@@ -171,4 +205,24 @@ class DeviceInfoService {
return false;
}
}
+
+ /// ๋ฐฑ์๋ ๋ก๊ทธ์ธ์ฉ ๊ธฐ๊ธฐ ์ ๋ณด๋ฅผ ์์งํฉ๋๋ค
+ ///
+ /// ๊ธฐ๊ธฐ ์ด๋ฆ, ํ์
, ๊ณ ์ ID, OS ๋ฒ์ ๋ฑ์ ํ ๋ฒ์ ์์งํฉ๋๋ค.
+ /// ๋ก๊ทธ์ธ ์ FCM ํ ํฐ๊ณผ ํจ๊ป ๋ฐฑ์๋์ ์ ์ก๋ฉ๋๋ค.
+ Future getDeviceInfo() async {
+ final deviceName = await getDeviceName();
+ final deviceType = getDeviceType();
+ final deviceId = await DeviceIdManager.getOrCreateDeviceId();
+ final osVersion = await getOSVersion();
+ final physical = await isPhysicalDevice();
+
+ return DeviceInfo(
+ deviceName: deviceName,
+ deviceType: deviceType,
+ deviceId: deviceId,
+ osVersion: osVersion,
+ isPhysicalDevice: physical,
+ );
+ }
}
diff --git a/lib/core/services/device/device_info_service.g.dart b/lib/core/services/device/device_info_service.g.dart
new file mode 100644
index 0000000..7818875
--- /dev/null
+++ b/lib/core/services/device/device_info_service.g.dart
@@ -0,0 +1,30 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'device_info_service.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$deviceInfoServiceHash() => r'694e64043cca19e34677981472b50d45bca72fc1';
+
+/// DeviceInfoService Provider
+///
+/// Copied from [deviceInfoService].
+@ProviderFor(deviceInfoService)
+final deviceInfoServiceProvider =
+ AutoDisposeProvider.internal(
+ deviceInfoService,
+ name: r'deviceInfoServiceProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$deviceInfoServiceHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+ );
+
+@Deprecated('Will be removed in 3.0. Use Ref instead')
+// ignore: unused_element
+typedef DeviceInfoServiceRef = AutoDisposeProviderRef;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
diff --git a/lib/core/services/fcm/firebase_messaging_service.dart b/lib/core/services/fcm/firebase_messaging_service.dart
index 8792081..046963e 100644
--- a/lib/core/services/fcm/firebase_messaging_service.dart
+++ b/lib/core/services/fcm/firebase_messaging_service.dart
@@ -1,10 +1,21 @@
import 'dart:async';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
import 'local_notifications_service.dart';
import '../device/device_info_service.dart';
import '../device/device_id_manager.dart';
+part 'firebase_messaging_service.g.dart';
+
+/// FirebaseMessagingService Provider
+@riverpod
+FirebaseMessagingService firebaseMessagingService(Ref ref) {
+ return FirebaseMessagingService.instance();
+}
+
/// Firebase Cloud Messaging ์๋น์ค
/// FCM ํธ์ ์๋ฆผ์ ๊ด๋ฆฌํ๊ณ ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค
class FirebaseMessagingService {
diff --git a/lib/core/services/fcm/firebase_messaging_service.g.dart b/lib/core/services/fcm/firebase_messaging_service.g.dart
new file mode 100644
index 0000000..1072e9f
--- /dev/null
+++ b/lib/core/services/fcm/firebase_messaging_service.g.dart
@@ -0,0 +1,32 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'firebase_messaging_service.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$firebaseMessagingServiceHash() =>
+ r'6b59f5611cd378acf599644d912cc77202a62abd';
+
+/// FirebaseMessagingService Provider
+///
+/// Copied from [firebaseMessagingService].
+@ProviderFor(firebaseMessagingService)
+final firebaseMessagingServiceProvider =
+ AutoDisposeProvider.internal(
+ firebaseMessagingService,
+ name: r'firebaseMessagingServiceProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$firebaseMessagingServiceHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+ );
+
+@Deprecated('Will be removed in 3.0. Use Ref instead')
+// ignore: unused_element
+typedef FirebaseMessagingServiceRef =
+ AutoDisposeProviderRef;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
diff --git a/lib/features/auth/data/datasources/auth_remote_datasource.dart b/lib/features/auth/data/datasources/auth_remote_datasource.dart
new file mode 100644
index 0000000..347826a
--- /dev/null
+++ b/lib/features/auth/data/datasources/auth_remote_datasource.dart
@@ -0,0 +1,89 @@
+import 'package:dio/dio.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../../../../core/constants/api_endpoints.dart';
+import '../../../../core/network/api_client.dart';
+import '../models/sign_in_request.dart';
+import '../models/sign_in_response.dart';
+import '../models/reissue_request.dart';
+import '../models/reissue_response.dart';
+
+part 'auth_remote_datasource.g.dart';
+
+/// ์ธ์ฆ Remote DataSource Provider
+@riverpod
+AuthRemoteDataSource authRemoteDataSource(Ref ref) {
+ final dio = ref.watch(dioProvider);
+ return AuthRemoteDataSource(dio);
+}
+
+/// ์ธ์ฆ ๊ด๋ จ ๋ฐฑ์๋ API ํธ์ถ
+///
+/// Firebase ์ธ์ฆ ํ ๋ฐฑ์๋ ์๋ฒ์ ํต์ ํ์ฌ
+/// JWT ํ ํฐ์ ๋ฐ๊ธ๋ฐ๊ณ ๊ด๋ฆฌํฉ๋๋ค.
+class AuthRemoteDataSource {
+ final Dio _dio;
+
+ AuthRemoteDataSource(this._dio);
+
+ /// ์์
๋ก๊ทธ์ธ (Firebase ID Token โ ๋ฐฑ์๋ JWT)
+ ///
+ /// [request]์ Firebase ID Token๊ณผ ๊ธฐ๊ธฐ ์ ๋ณด๋ฅผ ๋ด์ ์ ์กํฉ๋๋ค.
+ /// ์ฑ๊ณต ์ Access Token, Refresh Token, ์จ๋ณด๋ฉ ์ํ๋ฅผ ๋ฐํํฉ๋๋ค.
+ ///
+ /// Throws: DioException (network errors), AppException (converted errors)
+ Future signIn(SignInRequest request) async {
+ debugPrint('๐ Calling sign-in API...');
+ debugPrint(' Firebase ID Token length: ${request.firebaseIdToken.length}');
+
+ final response = await _dio.post(
+ ApiEndpoints.signIn,
+ data: request.toJson(),
+ );
+
+ debugPrint('โ
Sign-in API response: ${response.statusCode}');
+
+ return SignInResponse.fromJson(response.data);
+ }
+
+ /// ํ ํฐ ์ฌ๋ฐ๊ธ
+ ///
+ /// Refresh Token์ ์ฌ์ฉํ์ฌ ์๋ก์ด Access Token์ ๋ฐ๊ธ๋ฐ์ต๋๋ค.
+ /// ์ฃผ๋ก TokenRefreshInterceptor์์ ์๋์ผ๋ก ํธ์ถ๋ฉ๋๋ค.
+ Future reissue(ReissueRequest request) async {
+ debugPrint('๐ Calling reissue API...');
+
+ final response = await _dio.post(
+ ApiEndpoints.reissue,
+ data: request.toJson(),
+ );
+
+ debugPrint('โ
Reissue API response: ${response.statusCode}');
+
+ return ReissueResponse.fromJson(response.data);
+ }
+
+ /// ๋ก๊ทธ์์
+ ///
+ /// ์๋ฒ์ ๋ก๊ทธ์์์ ์๋ฆฌ๊ณ ํ ํฐ์ ๋ฌดํจํํฉ๋๋ค.
+ Future logout() async {
+ debugPrint('๐ช Calling logout API...');
+
+ await _dio.post(ApiEndpoints.logout);
+
+ debugPrint('โ
Logout API completed');
+ }
+
+ /// ํ์ ํํด
+ ///
+ /// ํ์ ์ ๋ณด๋ฅผ ์ญ์ ํฉ๋๋ค. (Soft Delete)
+ Future withdraw() async {
+ debugPrint('โ ๏ธ Calling withdraw API...');
+
+ await _dio.delete(ApiEndpoints.withdraw);
+
+ debugPrint('โ
Withdraw API completed');
+ }
+}
diff --git a/lib/features/auth/data/datasources/auth_remote_datasource.g.dart b/lib/features/auth/data/datasources/auth_remote_datasource.g.dart
new file mode 100644
index 0000000..c975356
--- /dev/null
+++ b/lib/features/auth/data/datasources/auth_remote_datasource.g.dart
@@ -0,0 +1,31 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'auth_remote_datasource.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$authRemoteDataSourceHash() =>
+ r'08fbc5fbf94c86964791b7f20b39f2504c0e8a74';
+
+/// ์ธ์ฆ Remote DataSource Provider
+///
+/// Copied from [authRemoteDataSource].
+@ProviderFor(authRemoteDataSource)
+final authRemoteDataSourceProvider =
+ AutoDisposeProvider.internal(
+ authRemoteDataSource,
+ name: r'authRemoteDataSourceProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$authRemoteDataSourceHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+ );
+
+@Deprecated('Will be removed in 3.0. Use Ref instead')
+// ignore: unused_element
+typedef AuthRemoteDataSourceRef = AutoDisposeProviderRef;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
diff --git a/lib/features/auth/data/models/reissue_request.dart b/lib/features/auth/data/models/reissue_request.dart
new file mode 100644
index 0000000..b71a943
--- /dev/null
+++ b/lib/features/auth/data/models/reissue_request.dart
@@ -0,0 +1,16 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'reissue_request.freezed.dart';
+part 'reissue_request.g.dart';
+
+/// ํ ํฐ ์ฌ๋ฐ๊ธ ์์ฒญ DTO
+@freezed
+class ReissueRequest with _$ReissueRequest {
+ const factory ReissueRequest({
+ /// Refresh Token
+ required String refreshToken,
+ }) = _ReissueRequest;
+
+ factory ReissueRequest.fromJson(Map json) =>
+ _$ReissueRequestFromJson(json);
+}
diff --git a/lib/features/auth/data/models/reissue_request.freezed.dart b/lib/features/auth/data/models/reissue_request.freezed.dart
new file mode 100644
index 0000000..eb1719f
--- /dev/null
+++ b/lib/features/auth/data/models/reissue_request.freezed.dart
@@ -0,0 +1,175 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'reissue_request.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
+);
+
+ReissueRequest _$ReissueRequestFromJson(Map json) {
+ return _ReissueRequest.fromJson(json);
+}
+
+/// @nodoc
+mixin _$ReissueRequest {
+ /// Refresh Token
+ String get refreshToken => throw _privateConstructorUsedError;
+
+ /// Serializes this ReissueRequest to a JSON map.
+ Map toJson() => throw _privateConstructorUsedError;
+
+ /// Create a copy of ReissueRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $ReissueRequestCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $ReissueRequestCopyWith<$Res> {
+ factory $ReissueRequestCopyWith(
+ ReissueRequest value,
+ $Res Function(ReissueRequest) then,
+ ) = _$ReissueRequestCopyWithImpl<$Res, ReissueRequest>;
+ @useResult
+ $Res call({String refreshToken});
+}
+
+/// @nodoc
+class _$ReissueRequestCopyWithImpl<$Res, $Val extends ReissueRequest>
+ implements $ReissueRequestCopyWith<$Res> {
+ _$ReissueRequestCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of ReissueRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({Object? refreshToken = null}) {
+ return _then(
+ _value.copyWith(
+ refreshToken: null == refreshToken
+ ? _value.refreshToken
+ : refreshToken // ignore: cast_nullable_to_non_nullable
+ as String,
+ )
+ as $Val,
+ );
+ }
+}
+
+/// @nodoc
+abstract class _$$ReissueRequestImplCopyWith<$Res>
+ implements $ReissueRequestCopyWith<$Res> {
+ factory _$$ReissueRequestImplCopyWith(
+ _$ReissueRequestImpl value,
+ $Res Function(_$ReissueRequestImpl) then,
+ ) = __$$ReissueRequestImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({String refreshToken});
+}
+
+/// @nodoc
+class __$$ReissueRequestImplCopyWithImpl<$Res>
+ extends _$ReissueRequestCopyWithImpl<$Res, _$ReissueRequestImpl>
+ implements _$$ReissueRequestImplCopyWith<$Res> {
+ __$$ReissueRequestImplCopyWithImpl(
+ _$ReissueRequestImpl _value,
+ $Res Function(_$ReissueRequestImpl) _then,
+ ) : super(_value, _then);
+
+ /// Create a copy of ReissueRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({Object? refreshToken = null}) {
+ return _then(
+ _$ReissueRequestImpl(
+ refreshToken: null == refreshToken
+ ? _value.refreshToken
+ : refreshToken // ignore: cast_nullable_to_non_nullable
+ as String,
+ ),
+ );
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$ReissueRequestImpl implements _ReissueRequest {
+ const _$ReissueRequestImpl({required this.refreshToken});
+
+ factory _$ReissueRequestImpl.fromJson(Map json) =>
+ _$$ReissueRequestImplFromJson(json);
+
+ /// Refresh Token
+ @override
+ final String refreshToken;
+
+ @override
+ String toString() {
+ return 'ReissueRequest(refreshToken: $refreshToken)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$ReissueRequestImpl &&
+ (identical(other.refreshToken, refreshToken) ||
+ other.refreshToken == refreshToken));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(runtimeType, refreshToken);
+
+ /// Create a copy of ReissueRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$ReissueRequestImplCopyWith<_$ReissueRequestImpl> get copyWith =>
+ __$$ReissueRequestImplCopyWithImpl<_$ReissueRequestImpl>(
+ this,
+ _$identity,
+ );
+
+ @override
+ Map toJson() {
+ return _$$ReissueRequestImplToJson(this);
+ }
+}
+
+abstract class _ReissueRequest implements ReissueRequest {
+ const factory _ReissueRequest({required final String refreshToken}) =
+ _$ReissueRequestImpl;
+
+ factory _ReissueRequest.fromJson(Map json) =
+ _$ReissueRequestImpl.fromJson;
+
+ /// Refresh Token
+ @override
+ String get refreshToken;
+
+ /// Create a copy of ReissueRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$ReissueRequestImplCopyWith<_$ReissueRequestImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/features/auth/data/models/reissue_request.g.dart b/lib/features/auth/data/models/reissue_request.g.dart
new file mode 100644
index 0000000..9cdbe42
--- /dev/null
+++ b/lib/features/auth/data/models/reissue_request.g.dart
@@ -0,0 +1,14 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'reissue_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$ReissueRequestImpl _$$ReissueRequestImplFromJson(Map json) =>
+ _$ReissueRequestImpl(refreshToken: json['refreshToken'] as String);
+
+Map _$$ReissueRequestImplToJson(
+ _$ReissueRequestImpl instance,
+) => {'refreshToken': instance.refreshToken};
diff --git a/lib/features/auth/data/models/reissue_response.dart b/lib/features/auth/data/models/reissue_response.dart
new file mode 100644
index 0000000..c149269
--- /dev/null
+++ b/lib/features/auth/data/models/reissue_response.dart
@@ -0,0 +1,19 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'reissue_response.freezed.dart';
+part 'reissue_response.g.dart';
+
+/// ํ ํฐ ์ฌ๋ฐ๊ธ ์๋ต DTO
+@freezed
+class ReissueResponse with _$ReissueResponse {
+ const factory ReissueResponse({
+ /// ์๋ก์ด Access Token
+ required String accessToken,
+
+ /// ์๋ก์ด Refresh Token
+ required String refreshToken,
+ }) = _ReissueResponse;
+
+ factory ReissueResponse.fromJson(Map json) =>
+ _$ReissueResponseFromJson(json);
+}
diff --git a/lib/features/auth/data/models/reissue_response.freezed.dart b/lib/features/auth/data/models/reissue_response.freezed.dart
new file mode 100644
index 0000000..833d379
--- /dev/null
+++ b/lib/features/auth/data/models/reissue_response.freezed.dart
@@ -0,0 +1,201 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'reissue_response.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
+);
+
+ReissueResponse _$ReissueResponseFromJson(Map json) {
+ return _ReissueResponse.fromJson(json);
+}
+
+/// @nodoc
+mixin _$ReissueResponse {
+ /// ์๋ก์ด Access Token
+ String get accessToken => throw _privateConstructorUsedError;
+
+ /// ์๋ก์ด Refresh Token
+ String get refreshToken => throw _privateConstructorUsedError;
+
+ /// Serializes this ReissueResponse to a JSON map.
+ Map toJson() => throw _privateConstructorUsedError;
+
+ /// Create a copy of ReissueResponse
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $ReissueResponseCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $ReissueResponseCopyWith<$Res> {
+ factory $ReissueResponseCopyWith(
+ ReissueResponse value,
+ $Res Function(ReissueResponse) then,
+ ) = _$ReissueResponseCopyWithImpl<$Res, ReissueResponse>;
+ @useResult
+ $Res call({String accessToken, String refreshToken});
+}
+
+/// @nodoc
+class _$ReissueResponseCopyWithImpl<$Res, $Val extends ReissueResponse>
+ implements $ReissueResponseCopyWith<$Res> {
+ _$ReissueResponseCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of ReissueResponse
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({Object? accessToken = null, Object? refreshToken = null}) {
+ return _then(
+ _value.copyWith(
+ accessToken: null == accessToken
+ ? _value.accessToken
+ : accessToken // ignore: cast_nullable_to_non_nullable
+ as String,
+ refreshToken: null == refreshToken
+ ? _value.refreshToken
+ : refreshToken // ignore: cast_nullable_to_non_nullable
+ as String,
+ )
+ as $Val,
+ );
+ }
+}
+
+/// @nodoc
+abstract class _$$ReissueResponseImplCopyWith<$Res>
+ implements $ReissueResponseCopyWith<$Res> {
+ factory _$$ReissueResponseImplCopyWith(
+ _$ReissueResponseImpl value,
+ $Res Function(_$ReissueResponseImpl) then,
+ ) = __$$ReissueResponseImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({String accessToken, String refreshToken});
+}
+
+/// @nodoc
+class __$$ReissueResponseImplCopyWithImpl<$Res>
+ extends _$ReissueResponseCopyWithImpl<$Res, _$ReissueResponseImpl>
+ implements _$$ReissueResponseImplCopyWith<$Res> {
+ __$$ReissueResponseImplCopyWithImpl(
+ _$ReissueResponseImpl _value,
+ $Res Function(_$ReissueResponseImpl) _then,
+ ) : super(_value, _then);
+
+ /// Create a copy of ReissueResponse
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({Object? accessToken = null, Object? refreshToken = null}) {
+ return _then(
+ _$ReissueResponseImpl(
+ accessToken: null == accessToken
+ ? _value.accessToken
+ : accessToken // ignore: cast_nullable_to_non_nullable
+ as String,
+ refreshToken: null == refreshToken
+ ? _value.refreshToken
+ : refreshToken // ignore: cast_nullable_to_non_nullable
+ as String,
+ ),
+ );
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$ReissueResponseImpl implements _ReissueResponse {
+ const _$ReissueResponseImpl({
+ required this.accessToken,
+ required this.refreshToken,
+ });
+
+ factory _$ReissueResponseImpl.fromJson(Map json) =>
+ _$$ReissueResponseImplFromJson(json);
+
+ /// ์๋ก์ด Access Token
+ @override
+ final String accessToken;
+
+ /// ์๋ก์ด Refresh Token
+ @override
+ final String refreshToken;
+
+ @override
+ String toString() {
+ return 'ReissueResponse(accessToken: $accessToken, refreshToken: $refreshToken)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$ReissueResponseImpl &&
+ (identical(other.accessToken, accessToken) ||
+ other.accessToken == accessToken) &&
+ (identical(other.refreshToken, refreshToken) ||
+ other.refreshToken == refreshToken));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(runtimeType, accessToken, refreshToken);
+
+ /// Create a copy of ReissueResponse
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$ReissueResponseImplCopyWith<_$ReissueResponseImpl> get copyWith =>
+ __$$ReissueResponseImplCopyWithImpl<_$ReissueResponseImpl>(
+ this,
+ _$identity,
+ );
+
+ @override
+ Map toJson() {
+ return _$$ReissueResponseImplToJson(this);
+ }
+}
+
+abstract class _ReissueResponse implements ReissueResponse {
+ const factory _ReissueResponse({
+ required final String accessToken,
+ required final String refreshToken,
+ }) = _$ReissueResponseImpl;
+
+ factory _ReissueResponse.fromJson(Map json) =
+ _$ReissueResponseImpl.fromJson;
+
+ /// ์๋ก์ด Access Token
+ @override
+ String get accessToken;
+
+ /// ์๋ก์ด Refresh Token
+ @override
+ String get refreshToken;
+
+ /// Create a copy of ReissueResponse
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$ReissueResponseImplCopyWith<_$ReissueResponseImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/features/auth/data/models/reissue_response.g.dart b/lib/features/auth/data/models/reissue_response.g.dart
new file mode 100644
index 0000000..155a694
--- /dev/null
+++ b/lib/features/auth/data/models/reissue_response.g.dart
@@ -0,0 +1,21 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'reissue_response.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$ReissueResponseImpl _$$ReissueResponseImplFromJson(
+ Map json,
+) => _$ReissueResponseImpl(
+ accessToken: json['accessToken'] as String,
+ refreshToken: json['refreshToken'] as String,
+);
+
+Map _$$ReissueResponseImplToJson(
+ _$ReissueResponseImpl instance,
+) => {
+ 'accessToken': instance.accessToken,
+ 'refreshToken': instance.refreshToken,
+};
diff --git a/lib/features/auth/data/models/sign_in_request.dart b/lib/features/auth/data/models/sign_in_request.dart
new file mode 100644
index 0000000..98317d5
--- /dev/null
+++ b/lib/features/auth/data/models/sign_in_request.dart
@@ -0,0 +1,35 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'sign_in_request.freezed.dart';
+part 'sign_in_request.g.dart';
+
+/// ๋ก๊ทธ์ธ ์์ฒญ DTO
+///
+/// Firebase ์ธ์ฆ ํ ๋ฐฑ์๋์ ์ ๋ฌํ ๋ก๊ทธ์ธ ์ ๋ณด์
๋๋ค.
+///
+/// **ํ์ ํ๋**:
+/// - [firebaseIdToken]: Firebase์์ ๋ฐ๊ธ๋ฐ์ ID Token
+///
+/// **์ ํ ํ๋**:
+/// - [fcmToken]: FCM ํธ์ ์๋ฆผ ํ ํฐ
+/// - [deviceType]: ๋๋ฐ์ด์ค ํ์
(IOS, ANDROID)
+/// - [deviceId]: ๋๋ฐ์ด์ค ๊ณ ์ ์๋ณ์
+@freezed
+class SignInRequest with _$SignInRequest {
+ const factory SignInRequest({
+ /// Firebase ID Token (ํ์)
+ required String firebaseIdToken,
+
+ /// FCM ํธ์ ์๋ฆผ ํ ํฐ (์ ํ)
+ String? fcmToken,
+
+ /// ๋๋ฐ์ด์ค ํ์
: IOS, ANDROID (์ ํ)
+ String? deviceType,
+
+ /// ๋๋ฐ์ด์ค ๊ณ ์ ์๋ณ์ UUID (์ ํ)
+ String? deviceId,
+ }) = _SignInRequest;
+
+ factory SignInRequest.fromJson(Map json) =>
+ _$SignInRequestFromJson(json);
+}
diff --git a/lib/features/auth/data/models/sign_in_request.freezed.dart b/lib/features/auth/data/models/sign_in_request.freezed.dart
new file mode 100644
index 0000000..ef9b5a0
--- /dev/null
+++ b/lib/features/auth/data/models/sign_in_request.freezed.dart
@@ -0,0 +1,265 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'sign_in_request.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
+);
+
+SignInRequest _$SignInRequestFromJson(Map json) {
+ return _SignInRequest.fromJson(json);
+}
+
+/// @nodoc
+mixin _$SignInRequest {
+ /// Firebase ID Token (ํ์)
+ String get firebaseIdToken => throw _privateConstructorUsedError;
+
+ /// FCM ํธ์ ์๋ฆผ ํ ํฐ (์ ํ)
+ String? get fcmToken => throw _privateConstructorUsedError;
+
+ /// ๋๋ฐ์ด์ค ํ์
: IOS, ANDROID (์ ํ)
+ String? get deviceType => throw _privateConstructorUsedError;
+
+ /// ๋๋ฐ์ด์ค ๊ณ ์ ์๋ณ์ UUID (์ ํ)
+ String? get deviceId => throw _privateConstructorUsedError;
+
+ /// Serializes this SignInRequest to a JSON map.
+ Map toJson() => throw _privateConstructorUsedError;
+
+ /// Create a copy of SignInRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $SignInRequestCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $SignInRequestCopyWith<$Res> {
+ factory $SignInRequestCopyWith(
+ SignInRequest value,
+ $Res Function(SignInRequest) then,
+ ) = _$SignInRequestCopyWithImpl<$Res, SignInRequest>;
+ @useResult
+ $Res call({
+ String firebaseIdToken,
+ String? fcmToken,
+ String? deviceType,
+ String? deviceId,
+ });
+}
+
+/// @nodoc
+class _$SignInRequestCopyWithImpl<$Res, $Val extends SignInRequest>
+ implements $SignInRequestCopyWith<$Res> {
+ _$SignInRequestCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of SignInRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? firebaseIdToken = null,
+ Object? fcmToken = freezed,
+ Object? deviceType = freezed,
+ Object? deviceId = freezed,
+ }) {
+ return _then(
+ _value.copyWith(
+ firebaseIdToken: null == firebaseIdToken
+ ? _value.firebaseIdToken
+ : firebaseIdToken // ignore: cast_nullable_to_non_nullable
+ as String,
+ fcmToken: freezed == fcmToken
+ ? _value.fcmToken
+ : fcmToken // ignore: cast_nullable_to_non_nullable
+ as String?,
+ deviceType: freezed == deviceType
+ ? _value.deviceType
+ : deviceType // ignore: cast_nullable_to_non_nullable
+ as String?,
+ deviceId: freezed == deviceId
+ ? _value.deviceId
+ : deviceId // ignore: cast_nullable_to_non_nullable
+ as String?,
+ )
+ as $Val,
+ );
+ }
+}
+
+/// @nodoc
+abstract class _$$SignInRequestImplCopyWith<$Res>
+ implements $SignInRequestCopyWith<$Res> {
+ factory _$$SignInRequestImplCopyWith(
+ _$SignInRequestImpl value,
+ $Res Function(_$SignInRequestImpl) then,
+ ) = __$$SignInRequestImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({
+ String firebaseIdToken,
+ String? fcmToken,
+ String? deviceType,
+ String? deviceId,
+ });
+}
+
+/// @nodoc
+class __$$SignInRequestImplCopyWithImpl<$Res>
+ extends _$SignInRequestCopyWithImpl<$Res, _$SignInRequestImpl>
+ implements _$$SignInRequestImplCopyWith<$Res> {
+ __$$SignInRequestImplCopyWithImpl(
+ _$SignInRequestImpl _value,
+ $Res Function(_$SignInRequestImpl) _then,
+ ) : super(_value, _then);
+
+ /// Create a copy of SignInRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? firebaseIdToken = null,
+ Object? fcmToken = freezed,
+ Object? deviceType = freezed,
+ Object? deviceId = freezed,
+ }) {
+ return _then(
+ _$SignInRequestImpl(
+ firebaseIdToken: null == firebaseIdToken
+ ? _value.firebaseIdToken
+ : firebaseIdToken // ignore: cast_nullable_to_non_nullable
+ as String,
+ fcmToken: freezed == fcmToken
+ ? _value.fcmToken
+ : fcmToken // ignore: cast_nullable_to_non_nullable
+ as String?,
+ deviceType: freezed == deviceType
+ ? _value.deviceType
+ : deviceType // ignore: cast_nullable_to_non_nullable
+ as String?,
+ deviceId: freezed == deviceId
+ ? _value.deviceId
+ : deviceId // ignore: cast_nullable_to_non_nullable
+ as String?,
+ ),
+ );
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$SignInRequestImpl implements _SignInRequest {
+ const _$SignInRequestImpl({
+ required this.firebaseIdToken,
+ this.fcmToken,
+ this.deviceType,
+ this.deviceId,
+ });
+
+ factory _$SignInRequestImpl.fromJson(Map json) =>
+ _$$SignInRequestImplFromJson(json);
+
+ /// Firebase ID Token (ํ์)
+ @override
+ final String firebaseIdToken;
+
+ /// FCM ํธ์ ์๋ฆผ ํ ํฐ (์ ํ)
+ @override
+ final String? fcmToken;
+
+ /// ๋๋ฐ์ด์ค ํ์
: IOS, ANDROID (์ ํ)
+ @override
+ final String? deviceType;
+
+ /// ๋๋ฐ์ด์ค ๊ณ ์ ์๋ณ์ UUID (์ ํ)
+ @override
+ final String? deviceId;
+
+ @override
+ String toString() {
+ return 'SignInRequest(firebaseIdToken: $firebaseIdToken, fcmToken: $fcmToken, deviceType: $deviceType, deviceId: $deviceId)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$SignInRequestImpl &&
+ (identical(other.firebaseIdToken, firebaseIdToken) ||
+ other.firebaseIdToken == firebaseIdToken) &&
+ (identical(other.fcmToken, fcmToken) ||
+ other.fcmToken == fcmToken) &&
+ (identical(other.deviceType, deviceType) ||
+ other.deviceType == deviceType) &&
+ (identical(other.deviceId, deviceId) ||
+ other.deviceId == deviceId));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode =>
+ Object.hash(runtimeType, firebaseIdToken, fcmToken, deviceType, deviceId);
+
+ /// Create a copy of SignInRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$SignInRequestImplCopyWith<_$SignInRequestImpl> get copyWith =>
+ __$$SignInRequestImplCopyWithImpl<_$SignInRequestImpl>(this, _$identity);
+
+ @override
+ Map toJson() {
+ return _$$SignInRequestImplToJson(this);
+ }
+}
+
+abstract class _SignInRequest implements SignInRequest {
+ const factory _SignInRequest({
+ required final String firebaseIdToken,
+ final String? fcmToken,
+ final String? deviceType,
+ final String? deviceId,
+ }) = _$SignInRequestImpl;
+
+ factory _SignInRequest.fromJson(Map json) =
+ _$SignInRequestImpl.fromJson;
+
+ /// Firebase ID Token (ํ์)
+ @override
+ String get firebaseIdToken;
+
+ /// FCM ํธ์ ์๋ฆผ ํ ํฐ (์ ํ)
+ @override
+ String? get fcmToken;
+
+ /// ๋๋ฐ์ด์ค ํ์
: IOS, ANDROID (์ ํ)
+ @override
+ String? get deviceType;
+
+ /// ๋๋ฐ์ด์ค ๊ณ ์ ์๋ณ์ UUID (์ ํ)
+ @override
+ String? get deviceId;
+
+ /// Create a copy of SignInRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$SignInRequestImplCopyWith<_$SignInRequestImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/features/auth/data/models/sign_in_request.g.dart b/lib/features/auth/data/models/sign_in_request.g.dart
new file mode 100644
index 0000000..572b9aa
--- /dev/null
+++ b/lib/features/auth/data/models/sign_in_request.g.dart
@@ -0,0 +1,23 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'sign_in_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$SignInRequestImpl _$$SignInRequestImplFromJson(Map json) =>
+ _$SignInRequestImpl(
+ firebaseIdToken: json['firebaseIdToken'] as String,
+ fcmToken: json['fcmToken'] as String?,
+ deviceType: json['deviceType'] as String?,
+ deviceId: json['deviceId'] as String?,
+ );
+
+Map _$$SignInRequestImplToJson(_$SignInRequestImpl instance) =>
+ {
+ 'firebaseIdToken': instance.firebaseIdToken,
+ 'fcmToken': instance.fcmToken,
+ 'deviceType': instance.deviceType,
+ 'deviceId': instance.deviceId,
+ };
diff --git a/lib/features/auth/data/models/sign_in_response.dart b/lib/features/auth/data/models/sign_in_response.dart
new file mode 100644
index 0000000..afbcffe
--- /dev/null
+++ b/lib/features/auth/data/models/sign_in_response.dart
@@ -0,0 +1,42 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+// OnboardingStep์ ํตํฉ enum ์ฌ์ฉ
+export '../../domain/entities/onboarding_step.dart';
+
+part 'sign_in_response.freezed.dart';
+part 'sign_in_response.g.dart';
+
+/// ๋ก๊ทธ์ธ ์๋ต DTO
+///
+/// ๋ฐฑ์๋ ๋ก๊ทธ์ธ ์ฑ๊ณต ์ ๋ฐํ๋๋ ์ ๋ณด์
๋๋ค.
+///
+/// **ํ ํฐ ์ ๋ณด**:
+/// - [accessToken]: API ์ธ์ฆ์ฉ JWT Access Token (์ฝ 1์๊ฐ ์ ํจ)
+/// - [refreshToken]: Access Token ๊ฐฑ์ ์ฉ Refresh Token (์ฝ 7์ผ ์ ํจ)
+///
+/// **์จ๋ณด๋ฉ ์ ๋ณด**:
+/// - [isFirstLogin]: ์ฒซ ๋ก๊ทธ์ธ ์ฌ๋ถ
+/// - [requiresOnboarding]: ์จ๋ณด๋ฉ ์๋ฃ ํ์ ์ฌ๋ถ
+/// - [onboardingStep]: ํ์ฌ ์จ๋ณด๋ฉ ๋จ๊ณ (TERMS, BIRTH_DATE, GENDER, NICKNAME)
+@freezed
+class SignInResponse with _$SignInResponse {
+ const factory SignInResponse({
+ /// API ์ธ์ฆ์ฉ JWT Access Token
+ required String accessToken,
+
+ /// Access Token ๊ฐฑ์ ์ฉ Refresh Token
+ required String refreshToken,
+
+ /// ์ฒซ ๋ก๊ทธ์ธ ์ฌ๋ถ
+ @Default(false) bool isFirstLogin,
+
+ /// ์จ๋ณด๋ฉ ์๋ฃ ํ์ ์ฌ๋ถ
+ @Default(false) bool requiresOnboarding,
+
+ /// ํ์ฌ ์จ๋ณด๋ฉ ๋จ๊ณ: TERMS, BIRTH_DATE, GENDER, NICKNAME, COMPLETED
+ String? onboardingStep,
+ }) = _SignInResponse;
+
+ factory SignInResponse.fromJson(Map json) =>
+ _$SignInResponseFromJson(json);
+}
diff --git a/lib/features/auth/data/models/sign_in_response.freezed.dart b/lib/features/auth/data/models/sign_in_response.freezed.dart
new file mode 100644
index 0000000..3ae50ae
--- /dev/null
+++ b/lib/features/auth/data/models/sign_in_response.freezed.dart
@@ -0,0 +1,303 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'sign_in_response.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
+);
+
+SignInResponse _$SignInResponseFromJson(Map json) {
+ return _SignInResponse.fromJson(json);
+}
+
+/// @nodoc
+mixin _$SignInResponse {
+ /// API ์ธ์ฆ์ฉ JWT Access Token
+ String get accessToken => throw _privateConstructorUsedError;
+
+ /// Access Token ๊ฐฑ์ ์ฉ Refresh Token
+ String get refreshToken => throw _privateConstructorUsedError;
+
+ /// ์ฒซ ๋ก๊ทธ์ธ ์ฌ๋ถ
+ bool get isFirstLogin => throw _privateConstructorUsedError;
+
+ /// ์จ๋ณด๋ฉ ์๋ฃ ํ์ ์ฌ๋ถ
+ bool get requiresOnboarding => throw _privateConstructorUsedError;
+
+ /// ํ์ฌ ์จ๋ณด๋ฉ ๋จ๊ณ: TERMS, BIRTH_DATE, GENDER, NICKNAME, COMPLETED
+ String? get onboardingStep => throw _privateConstructorUsedError;
+
+ /// Serializes this SignInResponse to a JSON map.
+ Map toJson() => throw _privateConstructorUsedError;
+
+ /// Create a copy of SignInResponse
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $SignInResponseCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $SignInResponseCopyWith<$Res> {
+ factory $SignInResponseCopyWith(
+ SignInResponse value,
+ $Res Function(SignInResponse) then,
+ ) = _$SignInResponseCopyWithImpl<$Res, SignInResponse>;
+ @useResult
+ $Res call({
+ String accessToken,
+ String refreshToken,
+ bool isFirstLogin,
+ bool requiresOnboarding,
+ String? onboardingStep,
+ });
+}
+
+/// @nodoc
+class _$SignInResponseCopyWithImpl<$Res, $Val extends SignInResponse>
+ implements $SignInResponseCopyWith<$Res> {
+ _$SignInResponseCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of SignInResponse
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? accessToken = null,
+ Object? refreshToken = null,
+ Object? isFirstLogin = null,
+ Object? requiresOnboarding = null,
+ Object? onboardingStep = freezed,
+ }) {
+ return _then(
+ _value.copyWith(
+ accessToken: null == accessToken
+ ? _value.accessToken
+ : accessToken // ignore: cast_nullable_to_non_nullable
+ as String,
+ refreshToken: null == refreshToken
+ ? _value.refreshToken
+ : refreshToken // ignore: cast_nullable_to_non_nullable
+ as String,
+ isFirstLogin: null == isFirstLogin
+ ? _value.isFirstLogin
+ : isFirstLogin // ignore: cast_nullable_to_non_nullable
+ as bool,
+ requiresOnboarding: null == requiresOnboarding
+ ? _value.requiresOnboarding
+ : requiresOnboarding // ignore: cast_nullable_to_non_nullable
+ as bool,
+ onboardingStep: freezed == onboardingStep
+ ? _value.onboardingStep
+ : onboardingStep // ignore: cast_nullable_to_non_nullable
+ as String?,
+ )
+ as $Val,
+ );
+ }
+}
+
+/// @nodoc
+abstract class _$$SignInResponseImplCopyWith<$Res>
+ implements $SignInResponseCopyWith<$Res> {
+ factory _$$SignInResponseImplCopyWith(
+ _$SignInResponseImpl value,
+ $Res Function(_$SignInResponseImpl) then,
+ ) = __$$SignInResponseImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({
+ String accessToken,
+ String refreshToken,
+ bool isFirstLogin,
+ bool requiresOnboarding,
+ String? onboardingStep,
+ });
+}
+
+/// @nodoc
+class __$$SignInResponseImplCopyWithImpl<$Res>
+ extends _$SignInResponseCopyWithImpl<$Res, _$SignInResponseImpl>
+ implements _$$SignInResponseImplCopyWith<$Res> {
+ __$$SignInResponseImplCopyWithImpl(
+ _$SignInResponseImpl _value,
+ $Res Function(_$SignInResponseImpl) _then,
+ ) : super(_value, _then);
+
+ /// Create a copy of SignInResponse
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? accessToken = null,
+ Object? refreshToken = null,
+ Object? isFirstLogin = null,
+ Object? requiresOnboarding = null,
+ Object? onboardingStep = freezed,
+ }) {
+ return _then(
+ _$SignInResponseImpl(
+ accessToken: null == accessToken
+ ? _value.accessToken
+ : accessToken // ignore: cast_nullable_to_non_nullable
+ as String,
+ refreshToken: null == refreshToken
+ ? _value.refreshToken
+ : refreshToken // ignore: cast_nullable_to_non_nullable
+ as String,
+ isFirstLogin: null == isFirstLogin
+ ? _value.isFirstLogin
+ : isFirstLogin // ignore: cast_nullable_to_non_nullable
+ as bool,
+ requiresOnboarding: null == requiresOnboarding
+ ? _value.requiresOnboarding
+ : requiresOnboarding // ignore: cast_nullable_to_non_nullable
+ as bool,
+ onboardingStep: freezed == onboardingStep
+ ? _value.onboardingStep
+ : onboardingStep // ignore: cast_nullable_to_non_nullable
+ as String?,
+ ),
+ );
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$SignInResponseImpl implements _SignInResponse {
+ const _$SignInResponseImpl({
+ required this.accessToken,
+ required this.refreshToken,
+ this.isFirstLogin = false,
+ this.requiresOnboarding = false,
+ this.onboardingStep,
+ });
+
+ factory _$SignInResponseImpl.fromJson(Map json) =>
+ _$$SignInResponseImplFromJson(json);
+
+ /// API ์ธ์ฆ์ฉ JWT Access Token
+ @override
+ final String accessToken;
+
+ /// Access Token ๊ฐฑ์ ์ฉ Refresh Token
+ @override
+ final String refreshToken;
+
+ /// ์ฒซ ๋ก๊ทธ์ธ ์ฌ๋ถ
+ @override
+ @JsonKey()
+ final bool isFirstLogin;
+
+ /// ์จ๋ณด๋ฉ ์๋ฃ ํ์ ์ฌ๋ถ
+ @override
+ @JsonKey()
+ final bool requiresOnboarding;
+
+ /// ํ์ฌ ์จ๋ณด๋ฉ ๋จ๊ณ: TERMS, BIRTH_DATE, GENDER, NICKNAME, COMPLETED
+ @override
+ final String? onboardingStep;
+
+ @override
+ String toString() {
+ return 'SignInResponse(accessToken: $accessToken, refreshToken: $refreshToken, isFirstLogin: $isFirstLogin, requiresOnboarding: $requiresOnboarding, onboardingStep: $onboardingStep)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$SignInResponseImpl &&
+ (identical(other.accessToken, accessToken) ||
+ other.accessToken == accessToken) &&
+ (identical(other.refreshToken, refreshToken) ||
+ other.refreshToken == refreshToken) &&
+ (identical(other.isFirstLogin, isFirstLogin) ||
+ other.isFirstLogin == isFirstLogin) &&
+ (identical(other.requiresOnboarding, requiresOnboarding) ||
+ other.requiresOnboarding == requiresOnboarding) &&
+ (identical(other.onboardingStep, onboardingStep) ||
+ other.onboardingStep == onboardingStep));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(
+ runtimeType,
+ accessToken,
+ refreshToken,
+ isFirstLogin,
+ requiresOnboarding,
+ onboardingStep,
+ );
+
+ /// Create a copy of SignInResponse
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$SignInResponseImplCopyWith<_$SignInResponseImpl> get copyWith =>
+ __$$SignInResponseImplCopyWithImpl<_$SignInResponseImpl>(
+ this,
+ _$identity,
+ );
+
+ @override
+ Map toJson() {
+ return _$$SignInResponseImplToJson(this);
+ }
+}
+
+abstract class _SignInResponse implements SignInResponse {
+ const factory _SignInResponse({
+ required final String accessToken,
+ required final String refreshToken,
+ final bool isFirstLogin,
+ final bool requiresOnboarding,
+ final String? onboardingStep,
+ }) = _$SignInResponseImpl;
+
+ factory _SignInResponse.fromJson(Map json) =
+ _$SignInResponseImpl.fromJson;
+
+ /// API ์ธ์ฆ์ฉ JWT Access Token
+ @override
+ String get accessToken;
+
+ /// Access Token ๊ฐฑ์ ์ฉ Refresh Token
+ @override
+ String get refreshToken;
+
+ /// ์ฒซ ๋ก๊ทธ์ธ ์ฌ๋ถ
+ @override
+ bool get isFirstLogin;
+
+ /// ์จ๋ณด๋ฉ ์๋ฃ ํ์ ์ฌ๋ถ
+ @override
+ bool get requiresOnboarding;
+
+ /// ํ์ฌ ์จ๋ณด๋ฉ ๋จ๊ณ: TERMS, BIRTH_DATE, GENDER, NICKNAME, COMPLETED
+ @override
+ String? get onboardingStep;
+
+ /// Create a copy of SignInResponse
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$SignInResponseImplCopyWith<_$SignInResponseImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/features/auth/data/models/sign_in_response.g.dart b/lib/features/auth/data/models/sign_in_response.g.dart
new file mode 100644
index 0000000..a10ed3d
--- /dev/null
+++ b/lib/features/auth/data/models/sign_in_response.g.dart
@@ -0,0 +1,26 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'sign_in_response.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$SignInResponseImpl _$$SignInResponseImplFromJson(Map json) =>
+ _$SignInResponseImpl(
+ accessToken: json['accessToken'] as String,
+ refreshToken: json['refreshToken'] as String,
+ isFirstLogin: json['isFirstLogin'] as bool? ?? false,
+ requiresOnboarding: json['requiresOnboarding'] as bool? ?? false,
+ onboardingStep: json['onboardingStep'] as String?,
+ );
+
+Map _$$SignInResponseImplToJson(
+ _$SignInResponseImpl instance,
+) => {
+ 'accessToken': instance.accessToken,
+ 'refreshToken': instance.refreshToken,
+ 'isFirstLogin': instance.isFirstLogin,
+ 'requiresOnboarding': instance.requiresOnboarding,
+ 'onboardingStep': instance.onboardingStep,
+};
diff --git a/lib/features/auth/data/repositories/auth_repository_impl.dart b/lib/features/auth/data/repositories/auth_repository_impl.dart
new file mode 100644
index 0000000..a8dcd59
--- /dev/null
+++ b/lib/features/auth/data/repositories/auth_repository_impl.dart
@@ -0,0 +1,129 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../../../../core/network/token_storage.dart';
+import '../../domain/repositories/auth_repository.dart';
+import '../datasources/auth_remote_datasource.dart';
+import '../models/sign_in_request.dart';
+import '../models/sign_in_response.dart';
+import '../models/reissue_request.dart';
+
+part 'auth_repository_impl.g.dart';
+
+/// AuthRepository Provider
+@riverpod
+AuthRepository authRepository(Ref ref) {
+ final remoteDataSource = ref.watch(authRemoteDataSourceProvider);
+ final tokenStorage = ref.watch(tokenStorageProvider);
+ return AuthRepositoryImpl(remoteDataSource, tokenStorage);
+}
+
+/// AuthRepository ๊ตฌํ์ฒด
+///
+/// ์ธ์ฆ ๊ด๋ จ ๋น์ฆ๋์ค ๋ก์ง์ ๊ตฌํํฉ๋๋ค.
+/// Remote DataSource์ Token Storage๋ฅผ ์กฐํฉํ์ฌ
+/// ์์ ํ ์ธ์ฆ ํ๋ก์ฐ๋ฅผ ์ ๊ณตํฉ๋๋ค.
+class AuthRepositoryImpl implements AuthRepository {
+ final AuthRemoteDataSource _remoteDataSource;
+ final TokenStorage _tokenStorage;
+
+ AuthRepositoryImpl(this._remoteDataSource, this._tokenStorage);
+
+ @override
+ Future signIn({
+ required String firebaseIdToken,
+ String? fcmToken,
+ String? deviceType,
+ String? deviceId,
+ }) async {
+ debugPrint('๐ AuthRepository: Starting sign-in flow...');
+
+ // 1. ๋ฐฑ์๋ ๋ก๊ทธ์ธ API ํธ์ถ
+ final request = SignInRequest(
+ firebaseIdToken: firebaseIdToken,
+ fcmToken: fcmToken,
+ deviceType: deviceType,
+ deviceId: deviceId,
+ );
+
+ final response = await _remoteDataSource.signIn(request);
+
+ // 2. ํ ํฐ ์ ์ฅ
+ await _tokenStorage.saveTokens(
+ accessToken: response.accessToken,
+ refreshToken: response.refreshToken,
+ );
+ debugPrint('โ
Tokens saved to secure storage');
+
+ // 3. ์จ๋ณด๋ฉ ์ํ ์ ์ฅ
+ await _tokenStorage.saveOnboardingState(
+ requiresOnboarding: response.requiresOnboarding,
+ onboardingStep: response.onboardingStep,
+ );
+ debugPrint('โ
Onboarding state saved: requiresOnboarding=${response.requiresOnboarding}');
+
+ return response;
+ }
+
+ @override
+ Future refreshTokens() async {
+ debugPrint('๐ AuthRepository: Refreshing tokens...');
+
+ final refreshToken = await _tokenStorage.getRefreshToken();
+ if (refreshToken == null) {
+ throw Exception('No refresh token available');
+ }
+
+ final request = ReissueRequest(refreshToken: refreshToken);
+ final response = await _remoteDataSource.reissue(request);
+
+ await _tokenStorage.saveTokens(
+ accessToken: response.accessToken,
+ refreshToken: response.refreshToken,
+ );
+ debugPrint('โ
Tokens refreshed and saved');
+ }
+
+ @override
+ Future logout() async {
+ debugPrint('๐ช AuthRepository: Logging out...');
+
+ try {
+ // ์๋ฒ์ ๋ก๊ทธ์์ ์๋ฆผ
+ await _remoteDataSource.logout();
+ } catch (e) {
+ // ์๋ฒ ๋ก๊ทธ์์ ์คํจํด๋ ๋ก์ปฌ ํ ํฐ์ ์ญ์
+ debugPrint('โ ๏ธ Server logout failed, clearing local tokens anyway: $e');
+ }
+
+ // ๋ก์ปฌ ํ ํฐ ์ญ์
+ await _tokenStorage.clearTokens();
+ debugPrint('โ
Local tokens cleared');
+ }
+
+ @override
+ Future withdraw() async {
+ debugPrint('โ ๏ธ AuthRepository: Withdrawing...');
+
+ await _remoteDataSource.withdraw();
+ await _tokenStorage.clearTokens();
+ debugPrint('โ
Account withdrawn and tokens cleared');
+ }
+
+ @override
+ Future hasValidTokens() async {
+ return await _tokenStorage.hasTokens();
+ }
+
+ @override
+ Future requiresOnboarding() async {
+ return await _tokenStorage.getRequiresOnboarding();
+ }
+
+ @override
+ Future getCurrentOnboardingStep() async {
+ final stepString = await _tokenStorage.getOnboardingStep();
+ return OnboardingStep.fromString(stepString);
+ }
+}
diff --git a/lib/features/auth/data/repositories/auth_repository_impl.g.dart b/lib/features/auth/data/repositories/auth_repository_impl.g.dart
new file mode 100644
index 0000000..c331a6e
--- /dev/null
+++ b/lib/features/auth/data/repositories/auth_repository_impl.g.dart
@@ -0,0 +1,29 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'auth_repository_impl.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$authRepositoryHash() => r'9c828bf1cc8b5f189768dbbb25dd06de52adb325';
+
+/// AuthRepository Provider
+///
+/// Copied from [authRepository].
+@ProviderFor(authRepository)
+final authRepositoryProvider = AutoDisposeProvider.internal(
+ authRepository,
+ name: r'authRepositoryProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$authRepositoryHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+@Deprecated('Will be removed in 3.0. Use Ref instead')
+// ignore: unused_element
+typedef AuthRepositoryRef = AutoDisposeProviderRef;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
diff --git a/lib/features/auth/domain/entities/onboarding_step.dart b/lib/features/auth/domain/entities/onboarding_step.dart
new file mode 100644
index 0000000..80ad2f5
--- /dev/null
+++ b/lib/features/auth/domain/entities/onboarding_step.dart
@@ -0,0 +1,123 @@
+/// ์จ๋ณด๋ฉ ๋จ๊ณ ์ด๊ฑฐํ (ํตํฉ)
+///
+/// ์ธ์ฆ ๋ชจ๋๊ณผ ์จ๋ณด๋ฉ ๋ชจ๋์์ ๊ณตํต์ผ๋ก ์ฌ์ฉํฉ๋๋ค.
+///
+/// **๋จ๊ณ**:
+/// - [terms]: ์ฝ๊ด ๋์
+/// - [birthDate]: ์๋
์์ผ ์
๋ ฅ
+/// - [gender]: ์ฑ๋ณ ์ ํ
+/// - [nickname]: ๋๋ค์ ์ค์
+/// - [completed]: ์จ๋ณด๋ฉ ์๋ฃ
+enum OnboardingStep {
+ terms,
+ birthDate,
+ gender,
+ nickname,
+ completed;
+
+ /// ์๋ฒ ์๋ต ๋ฌธ์์ด์ OnboardingStep์ผ๋ก ๋ณํ
+ ///
+ /// null ์
๋ ฅ ์ ๊ธฐ๋ณธ๊ฐ [terms] ๋ฐํ
+ static OnboardingStep fromString(String? value) {
+ if (value == null) return OnboardingStep.terms;
+
+ switch (value.toUpperCase()) {
+ case 'TERMS':
+ return OnboardingStep.terms;
+ case 'BIRTH_DATE':
+ return OnboardingStep.birthDate;
+ case 'GENDER':
+ return OnboardingStep.gender;
+ case 'NICKNAME':
+ return OnboardingStep.nickname;
+ case 'COMPLETED':
+ return OnboardingStep.completed;
+ default:
+ return OnboardingStep.terms;
+ }
+ }
+
+ /// ์๋ฒ ์๋ต ๋ฌธ์์ด์ OnboardingStep์ผ๋ก ๋ณํ (nullable)
+ static OnboardingStep? fromStringNullable(String? value) {
+ if (value == null) return null;
+
+ switch (value.toUpperCase()) {
+ case 'TERMS':
+ return OnboardingStep.terms;
+ case 'BIRTH_DATE':
+ return OnboardingStep.birthDate;
+ case 'GENDER':
+ return OnboardingStep.gender;
+ case 'NICKNAME':
+ return OnboardingStep.nickname;
+ case 'COMPLETED':
+ return OnboardingStep.completed;
+ default:
+ return null;
+ }
+ }
+
+ /// ๋ผ์ฐํธ ๊ฒฝ๋ก๋ก ๋ณํ
+ String toRoutePath() {
+ switch (this) {
+ case OnboardingStep.terms:
+ return '/onboarding/terms';
+ case OnboardingStep.birthDate:
+ return '/onboarding/birth-date';
+ case OnboardingStep.gender:
+ return '/onboarding/gender';
+ case OnboardingStep.nickname:
+ return '/onboarding/nickname';
+ case OnboardingStep.completed:
+ return '/home';
+ }
+ }
+
+ /// ๋ค์ ๋จ๊ณ ๋ฐํ
+ OnboardingStep? get next {
+ switch (this) {
+ case OnboardingStep.terms:
+ return OnboardingStep.birthDate;
+ case OnboardingStep.birthDate:
+ return OnboardingStep.gender;
+ case OnboardingStep.gender:
+ return OnboardingStep.nickname;
+ case OnboardingStep.nickname:
+ return OnboardingStep.completed;
+ case OnboardingStep.completed:
+ return null;
+ }
+ }
+
+ /// ๋จ๊ณ ์ธ๋ฑ์ค (0-4)
+ int get stepIndex {
+ switch (this) {
+ case OnboardingStep.terms:
+ return 0;
+ case OnboardingStep.birthDate:
+ return 1;
+ case OnboardingStep.gender:
+ return 2;
+ case OnboardingStep.nickname:
+ return 3;
+ case OnboardingStep.completed:
+ return 4;
+ }
+ }
+
+ /// ์๋ฒ ์ ์ก์ฉ ๋ฌธ์์ด๋ก ๋ณํ
+ String toServerString() {
+ switch (this) {
+ case OnboardingStep.terms:
+ return 'TERMS';
+ case OnboardingStep.birthDate:
+ return 'BIRTH_DATE';
+ case OnboardingStep.gender:
+ return 'GENDER';
+ case OnboardingStep.nickname:
+ return 'NICKNAME';
+ case OnboardingStep.completed:
+ return 'COMPLETED';
+ }
+ }
+}
diff --git a/lib/features/auth/domain/repositories/auth_repository.dart b/lib/features/auth/domain/repositories/auth_repository.dart
new file mode 100644
index 0000000..a48a0f0
--- /dev/null
+++ b/lib/features/auth/domain/repositories/auth_repository.dart
@@ -0,0 +1,50 @@
+import '../../../auth/data/models/sign_in_response.dart';
+
+/// ์ธ์ฆ Repository ์ธํฐํ์ด์ค
+///
+/// ์ธ์ฆ ๊ด๋ จ ๋น์ฆ๋์ค ๋ก์ง์ ๊ณ์ฝ์ ์ ์ํฉ๋๋ค.
+/// Clean Architecture์ Domain Layer์ ์์นํฉ๋๋ค.
+abstract class AuthRepository {
+ /// ์์
๋ก๊ทธ์ธ
+ ///
+ /// Firebase ์ธ์ฆ ํ ๋ฐฑ์๋์ ๋ก๊ทธ์ธ์ ์์ฒญํฉ๋๋ค.
+ ///
+ /// [firebaseIdToken]: Firebase์์ ๋ฐ๊ธ๋ฐ์ ID Token
+ /// [fcmToken]: FCM ํธ์ ์๋ฆผ ํ ํฐ (์ ํ)
+ /// [deviceType]: ๋๋ฐ์ด์ค ํ์
- IOS, ANDROID (์ ํ)
+ /// [deviceId]: ๋๋ฐ์ด์ค ๊ณ ์ ์๋ณ์ (์ ํ)
+ ///
+ /// Returns: ๋ก๊ทธ์ธ ์๋ต (ํ ํฐ + ์จ๋ณด๋ฉ ์ํ)
+ Future signIn({
+ required String firebaseIdToken,
+ String? fcmToken,
+ String? deviceType,
+ String? deviceId,
+ });
+
+ /// ํ ํฐ ์ฌ๋ฐ๊ธ
+ ///
+ /// Refresh Token์ ์ฌ์ฉํ์ฌ ์๋ก์ด ํ ํฐ ์์ ๋ฐ๊ธ๋ฐ์ต๋๋ค.
+ Future refreshTokens();
+
+ /// ๋ก๊ทธ์์
+ ///
+ /// ์๋ฒ์ ๋ก๊ทธ์์์ ์๋ฆฌ๊ณ ๋ก์ปฌ ํ ํฐ์ ์ญ์ ํฉ๋๋ค.
+ Future logout();
+
+ /// ํ์ ํํด
+ ///
+ /// ํ์ ์ ๋ณด๋ฅผ ์ญ์ ํ๊ณ ๋ก์ปฌ ํ ํฐ์ ์ ๋ฆฌํฉ๋๋ค.
+ Future withdraw();
+
+ /// ์ ์ฅ๋ ํ ํฐ ์กด์ฌ ์ฌ๋ถ ํ์ธ
+ ///
+ /// ์ฑ ์์ ์ ์๋ ๋ก๊ทธ์ธ ์ฌ๋ถ ํ๋จ์ ์ฌ์ฉํฉ๋๋ค.
+ Future hasValidTokens();
+
+ /// ์จ๋ณด๋ฉ ํ์ ์ฌ๋ถ ํ์ธ
+ Future requiresOnboarding();
+
+ /// ํ์ฌ ์จ๋ณด๋ฉ ๋จ๊ณ ์กฐํ
+ Future getCurrentOnboardingStep();
+}
diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart
index 40f24c2..21804b1 100644
--- a/lib/features/auth/presentation/pages/login_page.dart
+++ b/lib/features/auth/presentation/pages/login_page.dart
@@ -2,10 +2,13 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:go_router/go_router.dart';
import '../../../../core/constants/spacing_and_radius.dart';
import '../../../../core/constants/text_styles.dart';
import '../../../../core/errors/app_exception.dart';
+import '../../../../router/route_paths.dart';
+import '../../data/models/sign_in_response.dart';
import '../providers/auth_provider.dart';
/// Google ๋ก๊ทธ์ธ ํ๋ฉด
@@ -18,8 +21,8 @@ class LoginPage extends ConsumerWidget {
///
/// Google ๋ก๊ทธ์ธ์ ์ํํ๊ณ ์๋ฌ ๋ฐ์ ์ SnackBar๋ฅผ ํ์ํฉ๋๋ค.
Future _handleGoogleSignIn(BuildContext context, WidgetRef ref) async {
- // AuthNotifier๋ก ๋ก๊ทธ์ธ ์ํ
- await ref.read(authNotifierProvider.notifier).signInWithGoogle();
+ // AuthNotifier๋ก ๋ก๊ทธ์ธ ์ํ (๋ฐฑ์๋ ์๋ต ํฌํจ)
+ final signInResponse = await ref.read(authNotifierProvider.notifier).signInWithGoogle();
// ์๋ฌ ์ฒดํฌ ๋ฐ SnackBar ํ์
if (!context.mounted) return;
@@ -31,7 +34,6 @@ class LoginPage extends ConsumerWidget {
? (authState.error as AuthException).message
: '๋ก๊ทธ์ธ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.';
- //TODO: ์ค๋ต๋ฐ ๋์ค์ ๋์์ธ ๋ง๋ค์ด์ง๋ฉด ๋ฐ๋์ด์ผํจ.
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage, style: AppTextStyles.toast),
@@ -40,17 +42,21 @@ class LoginPage extends ConsumerWidget {
duration: const Duration(seconds: 3),
),
);
+ return;
}
- // ์ฑ๊ณต ์ GoRouter๊ฐ ์๋์ผ๋ก HomePage๋ก ๋ฆฌ๋ค์ด๋ ํธ
+ // ๋ก๊ทธ์ธ ์ฑ๊ณต ์ ์จ๋ณด๋ฉ ์ฌ๋ถ์ ๋ฐ๋ผ ๋ฆฌ๋ค์ด๋ ํธ
+ if (signInResponse != null) {
+ _navigateAfterLogin(context, signInResponse);
+ }
}
/// Apple ๋ก๊ทธ์ธ ๋ฒํผ ํธ๋ค๋ฌ
///
/// Apple ๋ก๊ทธ์ธ์ ์ํํ๊ณ ์๋ฌ ๋ฐ์ ์ SnackBar๋ฅผ ํ์ํฉ๋๋ค.
Future _handleAppleSignIn(BuildContext context, WidgetRef ref) async {
- // AuthNotifier๋ก ๋ก๊ทธ์ธ ์ํ
- await ref.read(authNotifierProvider.notifier).signInWithApple();
+ // AuthNotifier๋ก ๋ก๊ทธ์ธ ์ํ (๋ฐฑ์๋ ์๋ต ํฌํจ)
+ final signInResponse = await ref.read(authNotifierProvider.notifier).signInWithApple();
// ์๋ฌ ์ฒดํฌ ๋ฐ SnackBar ํ์
if (!context.mounted) return;
@@ -62,7 +68,6 @@ class LoginPage extends ConsumerWidget {
? (authState.error as AuthException).message
: 'Apple ๋ก๊ทธ์ธ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.';
- //TODO: ์ค๋ต๋ฐ ๋์ค์ ๋์์ธ ๋ง๋ค์ด์ง๋ฉด ๋ฐ๋์ด์ผํจ.
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage, style: AppTextStyles.toast),
@@ -71,9 +76,43 @@ class LoginPage extends ConsumerWidget {
duration: const Duration(seconds: 3),
),
);
+ return;
+ }
+
+ // ๋ก๊ทธ์ธ ์ฑ๊ณต ์ ์จ๋ณด๋ฉ ์ฌ๋ถ์ ๋ฐ๋ผ ๋ฆฌ๋ค์ด๋ ํธ
+ if (signInResponse != null) {
+ _navigateAfterLogin(context, signInResponse);
}
+ }
- // ์ฑ๊ณต ์ GoRouter๊ฐ ์๋์ผ๋ก HomePage๋ก ๋ฆฌ๋ค์ด๋ ํธ
+ /// ๋ก๊ทธ์ธ ์ฑ๊ณต ํ ๋ค๋น๊ฒ์ด์
์ฒ๋ฆฌ
+ ///
+ /// ์จ๋ณด๋ฉ ํ์ ์ฌ๋ถ์ ๋ฐ๋ผ ์ ์ ํ ํ๋ฉด์ผ๋ก ์ด๋ํฉ๋๋ค.
+ void _navigateAfterLogin(BuildContext context, SignInResponse response) {
+ if (response.requiresOnboarding) {
+ // ์จ๋ณด๋ฉ์ด ํ์ํ ๊ฒฝ์ฐ ํ์ฌ ๋จ๊ณ๋ก ์ด๋
+ final step = OnboardingStep.fromString(response.onboardingStep);
+ switch (step) {
+ case OnboardingStep.terms:
+ context.go(RoutePaths.onboardingTerms);
+ break;
+ case OnboardingStep.birthDate:
+ context.go(RoutePaths.onboardingBirthDate);
+ break;
+ case OnboardingStep.gender:
+ context.go(RoutePaths.onboardingGender);
+ break;
+ case OnboardingStep.nickname:
+ context.go(RoutePaths.onboardingNickname);
+ break;
+ case OnboardingStep.completed:
+ context.go(RoutePaths.home);
+ break;
+ }
+ } else {
+ // ์จ๋ณด๋ฉ์ด ์๋ฃ๋ ๊ฒฝ์ฐ ํ์ผ๋ก ์ด๋
+ context.go(RoutePaths.home);
+ }
}
@override
diff --git a/lib/features/auth/presentation/pages/splash_page.dart b/lib/features/auth/presentation/pages/splash_page.dart
index e0bb190..8a6a069 100644
--- a/lib/features/auth/presentation/pages/splash_page.dart
+++ b/lib/features/auth/presentation/pages/splash_page.dart
@@ -1,7 +1,11 @@
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../../router/route_paths.dart';
+import '../../domain/entities/onboarding_step.dart';
+import '../providers/auth_provider.dart';
/// ์ฑ ์์ ์ ์ด๊ธฐ ํ๋ฉด
///
@@ -9,34 +13,80 @@ import '../../../../router/route_paths.dart';
/// - ๋น์ธ์ฆ: login ํ๋ฉด์ผ๋ก ์ด๋
/// - ์ธ์ฆ + ์จ๋ณด๋ฉ ๋ฏธ์๋ฃ: onboarding ํ๋ฉด์ผ๋ก ์ด๋
/// - ์ธ์ฆ + ์จ๋ณด๋ฉ ์๋ฃ: home ํ๋ฉด์ผ๋ก ์ด๋
-class SplashPage extends StatefulWidget {
+class SplashPage extends ConsumerStatefulWidget {
const SplashPage({super.key});
@override
- State createState() => _SplashPageState();
+ ConsumerState createState() => _SplashPageState();
}
-class _SplashPageState extends State {
+class _SplashPageState extends ConsumerState {
@override
void initState() {
super.initState();
_navigateToNextScreen();
}
- /// 2์ด ํ ๋ค์ ํ๋ฉด์ผ๋ก ์๋ ์ด๋
+ /// ์ธ์ฆ ์ํ๋ฅผ ํ์ธํ๊ณ ์ ์ ํ ํ๋ฉด์ผ๋ก ์ด๋
Future _navigateToNextScreen() async {
+ // ์ต์ 2์ด๊ฐ ์คํ๋์ ํ์
await Future.delayed(const Duration(seconds: 2));
// Widget์ด ์ฌ์ ํ ๋ง์ดํธ๋์ด ์๋์ง ํ์ธ (๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง)
if (!mounted) return;
- // TODO: ์ธ์ฆ ์ํ์ ๋ฐ๋ผ ๋ถ๊ธฐ ์ฒ๋ฆฌ
- // ํ์ฌ๋ ๋ฌด์กฐ๊ฑด ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก ์ด๋
- // ํฅํ Auth Provider ๊ตฌํ ํ ๋ค์๊ณผ ๊ฐ์ด ๋ถ๊ธฐ:
- // - ๋น์ธ์ฆ โ login
- // - ์ธ์ฆ + ์จ๋ณด๋ฉ ๋ฏธ์๋ฃ โ onboarding
- // - ์ธ์ฆ + ์จ๋ณด๋ฉ ์๋ฃ โ home
- context.go(RoutePaths.login);
+ try {
+ final authNotifier = ref.read(authNotifierProvider.notifier);
+
+ // 1. ์ ์ฅ๋ ํ ํฐ์ผ๋ก ์๋ ๋ก๊ทธ์ธ ์๋
+ final hasValidTokens = await authNotifier.tryAutoLogin();
+ debugPrint('๐ Auto-login check: hasValidTokens=$hasValidTokens');
+
+ if (!mounted) return;
+
+ if (!hasValidTokens) {
+ // ํ ํฐ์ด ์๊ฑฐ๋ ์ ํจํ์ง ์์ โ ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก
+ debugPrint('๐ Navigating to login (no valid tokens)');
+ context.go(RoutePaths.login);
+ return;
+ }
+
+ // 2. ์จ๋ณด๋ฉ ํ์ ์ฌ๋ถ ํ์ธ
+ final requiresOnboarding = await authNotifier.checkRequiresOnboarding();
+ debugPrint('๐ Onboarding check: requiresOnboarding=$requiresOnboarding');
+
+ if (!mounted) return;
+
+ if (!requiresOnboarding) {
+ // ์จ๋ณด๋ฉ ์๋ฃ๋จ โ ํ ํ๋ฉด์ผ๋ก
+ debugPrint('๐ Navigating to home (onboarding completed)');
+ context.go(RoutePaths.home);
+ return;
+ }
+
+ // 3. ์จ๋ณด๋ฉ ๋ฏธ์๋ฃ โ ํ์ฌ ์จ๋ณด๋ฉ ๋จ๊ณ๋ก ์ด๋
+ final currentStep = await authNotifier.getCurrentOnboardingStep();
+ debugPrint('๐ Current onboarding step: $currentStep');
+
+ if (!mounted) return;
+
+ if (currentStep != null && currentStep != OnboardingStep.completed) {
+ // ํด๋น ์จ๋ณด๋ฉ ๋จ๊ณ๋ก ์ด๋
+ final routePath = currentStep.toRoutePath();
+ debugPrint('๐ Navigating to onboarding step: $routePath');
+ context.go(routePath);
+ } else {
+ // ๋จ๊ณ๋ฅผ ์ ์ ์์ผ๋ฉด ์ฝ๊ด ๋์๋ถํฐ ์์
+ debugPrint('๐ Navigating to onboarding terms (default)');
+ context.go(RoutePaths.onboardingTerms);
+ }
+ } catch (e) {
+ // ์๋ฌ ๋ฐ์ ์ ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก ์ด๋
+ debugPrint('โ Splash navigation error: $e');
+ if (mounted) {
+ context.go(RoutePaths.login);
+ }
+ }
}
@override
diff --git a/lib/features/auth/presentation/providers/auth_provider.dart b/lib/features/auth/presentation/providers/auth_provider.dart
index b653aab..3c9cdc2 100644
--- a/lib/features/auth/presentation/providers/auth_provider.dart
+++ b/lib/features/auth/presentation/providers/auth_provider.dart
@@ -4,7 +4,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../../../core/errors/app_exception.dart';
+import '../../../../core/network/token_storage.dart';
+import '../../../../core/services/device/device_info_service.dart';
+import '../../../../core/services/fcm/firebase_messaging_service.dart';
import '../../data/datasources/firebase_auth_datasource.dart';
+import '../../data/models/sign_in_response.dart';
+import '../../data/repositories/auth_repository_impl.dart';
import '../../domain/utils/firebase_auth_error_handler.dart';
part 'auth_provider.g.dart';
@@ -27,97 +32,138 @@ Stream authState(Ref ref) {
/// ์ธ์ฆ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ Notifier
///
-/// Google ๋ก๊ทธ์ธ, ๋ก๊ทธ์์ ๋ฑ์ ์ธ์ฆ ์์
์ ์ํํ๋ฉฐ
-/// ๋ก๋ฉ/์๋ฌ ์ํ๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
+/// Google/Apple ๋ก๊ทธ์ธ, ๋ก๊ทธ์์ ๋ฑ์ ์ธ์ฆ ์์
์ ์ํํ๋ฉฐ
+/// Firebase ์ธ์ฆ ํ ๋ฐฑ์๋ ๋ก๊ทธ์ธ๊น์ง ์๋ฃํฉ๋๋ค.
@riverpod
class AuthNotifier extends _$AuthNotifier {
@override
FutureOr build() {
- // Firebase Auth์ ํ์ฌ ์ฌ์ฉ์๋ฅผ ๋ฐํ
final dataSource = ref.watch(firebaseAuthDataSourceProvider);
return dataSource.currentUser;
}
/// Google ๋ก๊ทธ์ธ ์ํ
///
- /// ์ฌ์ฉ์๊ฐ ์ทจ์ํ๊ฑฐ๋ ๋คํธ์ํฌ ์ค๋ฅ ๋ฐ์ ์
- /// [AuthException]์ผ๋ก ๋ณํํ์ฌ ์๋ฌ ์ํ๋ฅผ ์ค์ ํฉ๋๋ค.
- ///
- /// ๋ก๊ทธ์ธ ์คํจ ์ Firebase/Google ์ธ์
์ ๋ชจ๋ ์ ๋ฆฌํฉ๋๋ค.
- Future signInWithGoogle() async {
+ /// 1. Firebase Google ๋ก๊ทธ์ธ
+ /// 2. Firebase ID Token ํ๋
+ /// 3. FCM ํ ํฐ + ๊ธฐ๊ธฐ ์ ๋ณด ์์ง
+ /// 4. ๋ฐฑ์๋ ๋ก๊ทธ์ธ API ํธ์ถ
+ /// 5. JWT ํ ํฐ ์ ์ฅ
+ /// 6. ์จ๋ณด๋ฉ ์ํ ์ ์ฅ
+ Future signInWithGoogle() async {
state = const AsyncValue.loading();
try {
final dataSource = ref.read(firebaseAuthDataSourceProvider);
+
+ // 1. Firebase Google ๋ก๊ทธ์ธ
final userCredential = await dataSource.signInWithGoogle();
+ debugPrint('โ
Firebase Google login success');
+
+ // 2. Firebase ID Token ํ๋
+ final idToken = await dataSource.getIdToken();
+ debugPrint('โ
Firebase ID Token obtained');
- // Firebase ID Token ๊ฒ์ฆ (์คํจ ์ ๋ด๋ถ์์ ์ธ์
์ ๋ฆฌ ์ํ)
- await _validateIdToken('google');
+ // 3. ๋ฐฑ์๋ ๋ก๊ทธ์ธ ์๋ฃ
+ final signInResponse = await _completeBackendLogin(idToken, 'google');
state = AsyncValue.data(userCredential.user);
+ return signInResponse;
} on FirebaseAuthException catch (e) {
- // ํ ํฐ ๊ฒ์ฆ ์คํจ๋ ์ด๋ฏธ _validateIdToken์์ ์ธ์
์ ๋ฆฌ๋จ
- // ๊ทธ ์ธ Firebase ์๋ฌ๋ง ์ฌ๊ธฐ์ ์ธ์
์ ๋ฆฌ
- if (e.code != 'token-validation-failed') {
- await _cleanupSessionOnFailure('google');
- }
-
- // Firebase ์๋ฌ๋ฅผ ์ฌ์ฉ์ ์นํ์ ๋ฉ์์ง๋ก ๋ณํ
+ await _cleanupSessionOnFailure('google');
state = AsyncValue.error(
FirebaseAuthErrorHandler.createAuthException(e, provider: 'Google'),
StackTrace.current,
);
+ return null;
} catch (e, stack) {
- // ์์์น ๋ชปํ ์๋ฌ ๋ฐ์ ์ ์ธ์
์ ๋ฆฌ
await _cleanupSessionOnFailure('google');
-
- // ๊ธฐํ ์์ธ ์ฒ๋ฆฌ
state = AsyncValue.error(
AuthException(message: '์ ์ ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.', originalException: e),
stack,
);
+ return null;
}
}
/// Apple ๋ก๊ทธ์ธ ์ํ
- ///
- /// ์ฌ์ฉ์๊ฐ ์ทจ์ํ๊ฑฐ๋ ๋คํธ์ํฌ ์ค๋ฅ ๋ฐ์ ์
- /// [AuthException]์ผ๋ก ๋ณํํ์ฌ ์๋ฌ ์ํ๋ฅผ ์ค์ ํฉ๋๋ค.
- ///
- /// ๋ก๊ทธ์ธ ์คํจ ์ Firebase ๋ฐ ์์
๋ก๊ทธ์ธ ์ธ์
์ ๋ชจ๋ ์ ๋ฆฌํฉ๋๋ค.
- Future signInWithApple() async {
+ Future signInWithApple() async {
state = const AsyncValue.loading();
try {
final dataSource = ref.read(firebaseAuthDataSourceProvider);
+
+ // 1. Firebase Apple ๋ก๊ทธ์ธ
final userCredential = await dataSource.signInWithApple();
+ debugPrint('โ
Firebase Apple login success');
+
+ // 2. Firebase ID Token ํ๋
+ final idToken = await dataSource.getIdToken();
+ debugPrint('โ
Firebase ID Token obtained');
- // Firebase ID Token ๊ฒ์ฆ (์คํจ ์ ๋ด๋ถ์์ ์ธ์
์ ๋ฆฌ ์ํ)
- await _validateIdToken('apple');
+ // 3. ๋ฐฑ์๋ ๋ก๊ทธ์ธ ์๋ฃ
+ final signInResponse = await _completeBackendLogin(idToken, 'apple');
state = AsyncValue.data(userCredential.user);
+ return signInResponse;
} on FirebaseAuthException catch (e) {
- // ํ ํฐ ๊ฒ์ฆ ์คํจ๋ ์ด๋ฏธ _validateIdToken์์ ์ธ์
์ ๋ฆฌ๋จ
- // ๊ทธ ์ธ Firebase ์๋ฌ๋ง ์ฌ๊ธฐ์ ์ธ์
์ ๋ฆฌ
- if (e.code != 'token-validation-failed') {
- await _cleanupSessionOnFailure('apple');
- }
-
- // Firebase ์๋ฌ๋ฅผ ์ฌ์ฉ์ ์นํ์ ๋ฉ์์ง๋ก ๋ณํ
+ await _cleanupSessionOnFailure('apple');
state = AsyncValue.error(
FirebaseAuthErrorHandler.createAuthException(e, provider: 'Apple'),
StackTrace.current,
);
+ return null;
} catch (e, stack) {
- // ์์์น ๋ชปํ ์๋ฌ ๋ฐ์ ์ ์ธ์
์ ๋ฆฌ
await _cleanupSessionOnFailure('apple');
-
- // ๊ธฐํ ์์ธ ์ฒ๋ฆฌ
state = AsyncValue.error(
AuthException(message: '์ ์ ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.', originalException: e),
stack,
);
+ return null;
+ }
+ }
+
+ /// ๋ฐฑ์๋ ๋ก๊ทธ์ธ ์๋ฃ (๊ณตํต ๋ก์ง)
+ Future _completeBackendLogin(
+ String idToken,
+ String provider,
+ ) async {
+ // FCM ํ ํฐ ํ๋
+ String? fcmToken;
+ try {
+ final fcmService = ref.read(firebaseMessagingServiceProvider);
+ fcmToken = await fcmService.getFcmToken();
+ debugPrint('โ
FCM Token obtained');
+ } catch (e) {
+ debugPrint('โ ๏ธ FCM Token retrieval failed (continuing): $e');
+ }
+
+ // ๊ธฐ๊ธฐ ์ ๋ณด ์์ง
+ String? deviceType;
+ String? deviceId;
+ try {
+ final deviceInfoService = ref.read(deviceInfoServiceProvider);
+ final deviceInfo = await deviceInfoService.getDeviceInfo();
+ deviceType = deviceInfo.deviceType;
+ deviceId = deviceInfo.deviceId;
+ debugPrint('โ
Device info obtained: $deviceType, $deviceId');
+ } catch (e) {
+ debugPrint('โ ๏ธ Device info retrieval failed (continuing): $e');
}
+
+ // ๋ฐฑ์๋ ๋ก๊ทธ์ธ API ํธ์ถ
+ final authRepository = ref.read(authRepositoryProvider);
+ final signInResponse = await authRepository.signIn(
+ firebaseIdToken: idToken,
+ fcmToken: fcmToken,
+ deviceType: deviceType,
+ deviceId: deviceId,
+ );
+ debugPrint('โ
Backend login success');
+ debugPrint(' requiresOnboarding: ${signInResponse.requiresOnboarding}');
+ debugPrint(' onboardingStep: ${signInResponse.onboardingStep}');
+
+ return signInResponse;
}
/// ๋ก๊ทธ์์
@@ -125,10 +171,23 @@ class AuthNotifier extends _$AuthNotifier {
state = const AsyncValue.loading();
try {
+ // Firebase ๋ก๊ทธ์์
final dataSource = ref.read(firebaseAuthDataSourceProvider);
await dataSource.signOut();
+
+ // ๋ฐฑ์๋ ๋ก๊ทธ์์ + ํ ํฐ ์ญ์
+ final authRepository = ref.read(authRepositoryProvider);
+ await authRepository.logout();
+
state = const AsyncValue.data(null);
+ debugPrint('โ
Sign out success');
} catch (e, stack) {
+ // ์๋ฌ๊ฐ ๋ฐ์ํด๋ ๋ก์ปฌ ํ ํฐ์ ์ญ์
+ try {
+ final tokenStorage = ref.read(tokenStorageProvider);
+ await tokenStorage.clearTokens();
+ } catch (_) {}
+
state = AsyncValue.error(
AuthException(message: '๋ก๊ทธ์์์ ์คํจํ์ต๋๋ค.', originalException: e),
stack,
@@ -136,47 +195,82 @@ class AuthNotifier extends _$AuthNotifier {
}
}
- /// ๋ก๊ทธ์ธ ์คํจ ์ ์ธ์
์ ๋ฆฌ
- ///
- /// Firebase ๋ฐ Google ์ธ์
์ ๋ชจ๋ ์ ๋ฆฌํฉ๋๋ค.
- /// ์๋ฌ๊ฐ ๋ฐ์ํด๋ ๋ฌด์ํ๊ณ ๊ณ์ ์งํํฉ๋๋ค.
- ///
- /// [provider]: ๋ก๊ทธ์ธ ์ ๊ณต์ ์ด๋ฆ (๋ก๊ทธ ์ถ๋ ฅ์ฉ)
- Future _cleanupSessionOnFailure(String provider) async {
+ /// ํ์ ํํด
+ Future withdraw() async {
+ state = const AsyncValue.loading();
+
try {
+ // ๋ฐฑ์๋ ํ์ ํํด
+ final authRepository = ref.read(authRepositoryProvider);
+ await authRepository.withdraw();
+
+ // Firebase ๋ก๊ทธ์์
final dataSource = ref.read(firebaseAuthDataSourceProvider);
await dataSource.signOut();
- debugPrint('๐ ๋ก๊ทธ์ธ ์คํจ - Firebase/Google ์ธ์
์ ๋ฆฌ ์๋ฃ ($provider)');
- } catch (signOutError) {
- debugPrint('โ ๏ธ ๋ก๊ทธ์์ ์ค ์๋ฌ (๋ฌด์): $signOutError');
+
+ state = const AsyncValue.data(null);
+ debugPrint('โ
Withdraw success');
+ } catch (e, stack) {
+ state = AsyncValue.error(
+ AuthException(message: 'ํ์ ํํด์ ์คํจํ์ต๋๋ค.', originalException: e),
+ stack,
+ );
}
}
- /// ๋ก๊ทธ์ธ ํ Firebase ID Token ๊ฒ์ฆ
- ///
- /// ํ ํฐ ๋ฐ๊ธ ์คํจ ์ ์ธ์
์ ์ ๋ฆฌํ๊ณ ๋ช
์์ ์ธ FirebaseAuthException์ ๋ฐ์์ํต๋๋ค.
- /// ์์ ํธ์ถ์๋ ํ ํฐ ๊ฒ์ฆ ์คํจ(code: 'token-validation-failed')์ ๋ํ
- /// ์ธ์
์ ๋ฆฌ๋ฅผ ์ ๊ฒฝ์ฐ์ง ์์๋ ๋ฉ๋๋ค.
- ///
- /// [provider]: ๋ก๊ทธ์ธ ์ ๊ณต์ ์ด๋ฆ (๋ก๊ทธ ์ถ๋ ฅ์ฉ)
- ///
- /// Throws: [FirebaseAuthException] (code: 'token-validation-failed') ํ ํฐ ๋ฐ๊ธ ์คํจ ์
- Future _validateIdToken(String provider) async {
+ /// ์ ์ฅ๋ ํ ํฐ์ผ๋ก ์๋ ๋ก๊ทธ์ธ ์๋
+ Future tryAutoLogin() async {
+ try {
+ final authRepository = ref.read(authRepositoryProvider);
+ final hasTokens = await authRepository.hasValidTokens();
+
+ if (hasTokens) {
+ debugPrint('โ
Valid tokens found, auto-login enabled');
+ return true;
+ }
+
+ debugPrint('โ ๏ธ No valid tokens found');
+ return false;
+ } catch (e) {
+ debugPrint('โ Auto-login check failed: $e');
+ return false;
+ }
+ }
+
+ /// ์จ๋ณด๋ฉ ํ์ ์ฌ๋ถ ํ์ธ
+ Future checkRequiresOnboarding() async {
+ try {
+ final authRepository = ref.read(authRepositoryProvider);
+ return await authRepository.requiresOnboarding();
+ } catch (e) {
+ debugPrint('โ Onboarding check failed: $e');
+ return false;
+ }
+ }
+
+ /// ํ์ฌ ์จ๋ณด๋ฉ ๋จ๊ณ ์กฐํ
+ Future getCurrentOnboardingStep() async {
+ try {
+ final authRepository = ref.read(authRepositoryProvider);
+ return await authRepository.getCurrentOnboardingStep();
+ } catch (e) {
+ debugPrint('โ Get onboarding step failed: $e');
+ return null;
+ }
+ }
+
+ /// ๋ก๊ทธ์ธ ์คํจ ์ ์ธ์
์ ๋ฆฌ
+ Future _cleanupSessionOnFailure(String provider) async {
try {
final dataSource = ref.read(firebaseAuthDataSourceProvider);
- await dataSource.getIdToken();
- } catch (tokenError) {
- debugPrint('โ Firebase ID Token ๋ฐ๊ธ ์คํจ - ๋ก๊ทธ์ธ ์ทจ์ ๋ฐ ๋ก๊ทธ์์ ์ฒ๋ฆฌ ($provider)');
- debugPrint('์๋ฌ ํ์
: ${tokenError.runtimeType}');
- debugPrint('์๋ฌ ์์ธ: $tokenError');
- await _cleanupSessionOnFailure(provider);
-
- // ํ ํฐ ๋ฐ๊ธ ์คํจ๋ฅผ ๋ช
์์ ์ธ FirebaseAuthException์ผ๋ก ๋ณํ
- // ์ด๋ฅผ ํตํด ์์ ํธ์ถ์์์ ์ผ๊ด๋ ์๋ฌ ์ฒ๋ฆฌ ๊ฐ๋ฅ
- throw FirebaseAuthException(
- code: 'token-validation-failed',
- message: 'Firebase ID Token ๋ฐ๊ธ์ ์คํจํ์ต๋๋ค.',
- );
+ await dataSource.signOut();
+
+ final tokenStorage = ref.read(tokenStorageProvider);
+ await tokenStorage.clearTokens();
+
+ debugPrint('๐ ๋ก๊ทธ์ธ ์คํจ - ์ธ์
์ ๋ฆฌ ์๋ฃ ($provider)');
+ } catch (signOutError) {
+ debugPrint('โ ๏ธ ๋ก๊ทธ์์ ์ค ์๋ฌ (๋ฌด์): $signOutError');
}
}
}
diff --git a/lib/features/onboarding/data/datasources/onboarding_remote_datasource.dart b/lib/features/onboarding/data/datasources/onboarding_remote_datasource.dart
new file mode 100644
index 0000000..52ac270
--- /dev/null
+++ b/lib/features/onboarding/data/datasources/onboarding_remote_datasource.dart
@@ -0,0 +1,102 @@
+import 'package:dio/dio.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../../../../core/constants/api_endpoints.dart';
+import '../../../../core/network/api_client.dart';
+import '../models/terms_request.dart';
+import '../models/birth_date_request.dart';
+import '../models/gender_request.dart';
+import '../models/profile_request.dart';
+
+part 'onboarding_remote_datasource.g.dart';
+
+/// OnboardingRemoteDataSource Provider
+@riverpod
+OnboardingRemoteDataSource onboardingRemoteDataSource(Ref ref) {
+ final dio = ref.watch(dioProvider);
+ return OnboardingRemoteDataSource(dio);
+}
+
+/// ์จ๋ณด๋ฉ Remote DataSource
+///
+/// ๋ฐฑ์๋ ์จ๋ณด๋ฉ API๋ฅผ ํธ์ถํฉ๋๋ค.
+class OnboardingRemoteDataSource {
+ final Dio _dio;
+
+ OnboardingRemoteDataSource(this._dio);
+
+ /// ์ฝ๊ด ๋์ ์ ์ถ
+ ///
+ /// POST /api/members/onboarding/terms
+ Future submitTerms(TermsRequest request) async {
+ debugPrint('๐ค OnboardingRemoteDataSource: Submitting terms agreement...');
+
+ await _dio.post(
+ ApiEndpoints.onboardingTerms,
+ data: request.toJson(),
+ );
+
+ debugPrint('โ
Terms agreement submitted successfully');
+ }
+
+ /// ์๋
์์ผ ์ ์ถ
+ ///
+ /// POST /api/members/onboarding/birth-date
+ Future submitBirthDate(BirthDateRequest request) async {
+ debugPrint('๐ค OnboardingRemoteDataSource: Submitting birth date...');
+
+ await _dio.post(
+ ApiEndpoints.onboardingBirthDate,
+ data: request.toJson(),
+ );
+
+ debugPrint('โ
Birth date submitted successfully');
+ }
+
+ /// ์ฑ๋ณ ์ ์ถ
+ ///
+ /// POST /api/members/onboarding/gender
+ Future submitGender(GenderRequest request) async {
+ debugPrint('๐ค OnboardingRemoteDataSource: Submitting gender...');
+
+ await _dio.post(
+ ApiEndpoints.onboardingGender,
+ data: request.toJson(),
+ );
+
+ debugPrint('โ
Gender submitted successfully');
+ }
+
+ /// ํ๋กํ(๋๋ค์) ์ ์ถ
+ ///
+ /// POST /api/members/profile
+ Future submitProfile(ProfileRequest request) async {
+ debugPrint('๐ค OnboardingRemoteDataSource: Submitting profile...');
+
+ await _dio.post(
+ ApiEndpoints.memberProfile,
+ data: request.toJson(),
+ );
+
+ debugPrint('โ
Profile submitted successfully');
+ }
+
+ /// ๋๋ค์ ์ค๋ณต ํ์ธ
+ ///
+ /// GET /api/members/check-name?name=xxx
+ Future checkName(String name) async {
+ debugPrint('๐ค OnboardingRemoteDataSource: Checking name availability...');
+
+ final response = await _dio.get(
+ ApiEndpoints.checkName,
+ queryParameters: {'name': name},
+ );
+
+ final checkNameResponse = CheckNameResponse.fromJson(response.data);
+ debugPrint('โ
Name check result: ${checkNameResponse.available}');
+
+ return checkNameResponse;
+ }
+}
diff --git a/lib/features/onboarding/data/datasources/onboarding_remote_datasource.g.dart b/lib/features/onboarding/data/datasources/onboarding_remote_datasource.g.dart
new file mode 100644
index 0000000..1678e2e
--- /dev/null
+++ b/lib/features/onboarding/data/datasources/onboarding_remote_datasource.g.dart
@@ -0,0 +1,32 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'onboarding_remote_datasource.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$onboardingRemoteDataSourceHash() =>
+ r'93c34981392d0b5d46a33b6cc2ebc536130e46b4';
+
+/// OnboardingRemoteDataSource Provider
+///
+/// Copied from [onboardingRemoteDataSource].
+@ProviderFor(onboardingRemoteDataSource)
+final onboardingRemoteDataSourceProvider =
+ AutoDisposeProvider.internal(
+ onboardingRemoteDataSource,
+ name: r'onboardingRemoteDataSourceProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$onboardingRemoteDataSourceHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+ );
+
+@Deprecated('Will be removed in 3.0. Use Ref instead')
+// ignore: unused_element
+typedef OnboardingRemoteDataSourceRef =
+ AutoDisposeProviderRef;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
diff --git a/lib/features/onboarding/data/models/birth_date_request.dart b/lib/features/onboarding/data/models/birth_date_request.dart
new file mode 100644
index 0000000..acdec05
--- /dev/null
+++ b/lib/features/onboarding/data/models/birth_date_request.dart
@@ -0,0 +1,18 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'birth_date_request.freezed.dart';
+part 'birth_date_request.g.dart';
+
+/// ์๋
์์ผ ์์ฒญ ๋ชจ๋ธ
+///
+/// POST /api/members/onboarding/birth-date ์์ฒญ ๋ฐ๋
+@freezed
+class BirthDateRequest with _$BirthDateRequest {
+ const factory BirthDateRequest({
+ /// ์๋
์์ผ (YYYY-MM-DD ํ์)
+ required String birthDate,
+ }) = _BirthDateRequest;
+
+ factory BirthDateRequest.fromJson(Map json) =>
+ _$BirthDateRequestFromJson(json);
+}
diff --git a/lib/features/onboarding/data/models/birth_date_request.freezed.dart b/lib/features/onboarding/data/models/birth_date_request.freezed.dart
new file mode 100644
index 0000000..74e664c
--- /dev/null
+++ b/lib/features/onboarding/data/models/birth_date_request.freezed.dart
@@ -0,0 +1,175 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'birth_date_request.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
+);
+
+BirthDateRequest _$BirthDateRequestFromJson(Map json) {
+ return _BirthDateRequest.fromJson(json);
+}
+
+/// @nodoc
+mixin _$BirthDateRequest {
+ /// ์๋
์์ผ (YYYY-MM-DD ํ์)
+ String get birthDate => throw _privateConstructorUsedError;
+
+ /// Serializes this BirthDateRequest to a JSON map.
+ Map toJson() => throw _privateConstructorUsedError;
+
+ /// Create a copy of BirthDateRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $BirthDateRequestCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $BirthDateRequestCopyWith<$Res> {
+ factory $BirthDateRequestCopyWith(
+ BirthDateRequest value,
+ $Res Function(BirthDateRequest) then,
+ ) = _$BirthDateRequestCopyWithImpl<$Res, BirthDateRequest>;
+ @useResult
+ $Res call({String birthDate});
+}
+
+/// @nodoc
+class _$BirthDateRequestCopyWithImpl<$Res, $Val extends BirthDateRequest>
+ implements $BirthDateRequestCopyWith<$Res> {
+ _$BirthDateRequestCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of BirthDateRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({Object? birthDate = null}) {
+ return _then(
+ _value.copyWith(
+ birthDate: null == birthDate
+ ? _value.birthDate
+ : birthDate // ignore: cast_nullable_to_non_nullable
+ as String,
+ )
+ as $Val,
+ );
+ }
+}
+
+/// @nodoc
+abstract class _$$BirthDateRequestImplCopyWith<$Res>
+ implements $BirthDateRequestCopyWith<$Res> {
+ factory _$$BirthDateRequestImplCopyWith(
+ _$BirthDateRequestImpl value,
+ $Res Function(_$BirthDateRequestImpl) then,
+ ) = __$$BirthDateRequestImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({String birthDate});
+}
+
+/// @nodoc
+class __$$BirthDateRequestImplCopyWithImpl<$Res>
+ extends _$BirthDateRequestCopyWithImpl<$Res, _$BirthDateRequestImpl>
+ implements _$$BirthDateRequestImplCopyWith<$Res> {
+ __$$BirthDateRequestImplCopyWithImpl(
+ _$BirthDateRequestImpl _value,
+ $Res Function(_$BirthDateRequestImpl) _then,
+ ) : super(_value, _then);
+
+ /// Create a copy of BirthDateRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({Object? birthDate = null}) {
+ return _then(
+ _$BirthDateRequestImpl(
+ birthDate: null == birthDate
+ ? _value.birthDate
+ : birthDate // ignore: cast_nullable_to_non_nullable
+ as String,
+ ),
+ );
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$BirthDateRequestImpl implements _BirthDateRequest {
+ const _$BirthDateRequestImpl({required this.birthDate});
+
+ factory _$BirthDateRequestImpl.fromJson(Map json) =>
+ _$$BirthDateRequestImplFromJson(json);
+
+ /// ์๋
์์ผ (YYYY-MM-DD ํ์)
+ @override
+ final String birthDate;
+
+ @override
+ String toString() {
+ return 'BirthDateRequest(birthDate: $birthDate)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$BirthDateRequestImpl &&
+ (identical(other.birthDate, birthDate) ||
+ other.birthDate == birthDate));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(runtimeType, birthDate);
+
+ /// Create a copy of BirthDateRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$BirthDateRequestImplCopyWith<_$BirthDateRequestImpl> get copyWith =>
+ __$$BirthDateRequestImplCopyWithImpl<_$BirthDateRequestImpl>(
+ this,
+ _$identity,
+ );
+
+ @override
+ Map toJson() {
+ return _$$BirthDateRequestImplToJson(this);
+ }
+}
+
+abstract class _BirthDateRequest implements BirthDateRequest {
+ const factory _BirthDateRequest({required final String birthDate}) =
+ _$BirthDateRequestImpl;
+
+ factory _BirthDateRequest.fromJson(Map json) =
+ _$BirthDateRequestImpl.fromJson;
+
+ /// ์๋
์์ผ (YYYY-MM-DD ํ์)
+ @override
+ String get birthDate;
+
+ /// Create a copy of BirthDateRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$BirthDateRequestImplCopyWith<_$BirthDateRequestImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/features/onboarding/data/models/birth_date_request.g.dart b/lib/features/onboarding/data/models/birth_date_request.g.dart
new file mode 100644
index 0000000..2c846f5
--- /dev/null
+++ b/lib/features/onboarding/data/models/birth_date_request.g.dart
@@ -0,0 +1,15 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'birth_date_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$BirthDateRequestImpl _$$BirthDateRequestImplFromJson(
+ Map json,
+) => _$BirthDateRequestImpl(birthDate: json['birthDate'] as String);
+
+Map _$$BirthDateRequestImplToJson(
+ _$BirthDateRequestImpl instance,
+) => {'birthDate': instance.birthDate};
diff --git a/lib/features/onboarding/data/models/gender_request.dart b/lib/features/onboarding/data/models/gender_request.dart
new file mode 100644
index 0000000..1223278
--- /dev/null
+++ b/lib/features/onboarding/data/models/gender_request.dart
@@ -0,0 +1,28 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'gender_request.freezed.dart';
+part 'gender_request.g.dart';
+
+/// ์ฑ๋ณ ์ ํ
+enum Gender {
+ @JsonValue('MALE')
+ male,
+ @JsonValue('FEMALE')
+ female,
+ @JsonValue('OTHER')
+ other,
+}
+
+/// ์ฑ๋ณ ์์ฒญ ๋ชจ๋ธ
+///
+/// POST /api/members/onboarding/gender ์์ฒญ ๋ฐ๋
+@freezed
+class GenderRequest with _$GenderRequest {
+ const factory GenderRequest({
+ /// ์ฑ๋ณ (MALE, FEMALE, OTHER)
+ required Gender gender,
+ }) = _GenderRequest;
+
+ factory GenderRequest.fromJson(Map json) =>
+ _$GenderRequestFromJson(json);
+}
diff --git a/lib/features/onboarding/data/models/gender_request.freezed.dart b/lib/features/onboarding/data/models/gender_request.freezed.dart
new file mode 100644
index 0000000..bbcbc97
--- /dev/null
+++ b/lib/features/onboarding/data/models/gender_request.freezed.dart
@@ -0,0 +1,171 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'gender_request.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
+);
+
+GenderRequest _$GenderRequestFromJson(Map json) {
+ return _GenderRequest.fromJson(json);
+}
+
+/// @nodoc
+mixin _$GenderRequest {
+ /// ์ฑ๋ณ (MALE, FEMALE, OTHER)
+ Gender get gender => throw _privateConstructorUsedError;
+
+ /// Serializes this GenderRequest to a JSON map.
+ Map toJson() => throw _privateConstructorUsedError;
+
+ /// Create a copy of GenderRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $GenderRequestCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $GenderRequestCopyWith<$Res> {
+ factory $GenderRequestCopyWith(
+ GenderRequest value,
+ $Res Function(GenderRequest) then,
+ ) = _$GenderRequestCopyWithImpl<$Res, GenderRequest>;
+ @useResult
+ $Res call({Gender gender});
+}
+
+/// @nodoc
+class _$GenderRequestCopyWithImpl<$Res, $Val extends GenderRequest>
+ implements $GenderRequestCopyWith<$Res> {
+ _$GenderRequestCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of GenderRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({Object? gender = null}) {
+ return _then(
+ _value.copyWith(
+ gender: null == gender
+ ? _value.gender
+ : gender // ignore: cast_nullable_to_non_nullable
+ as Gender,
+ )
+ as $Val,
+ );
+ }
+}
+
+/// @nodoc
+abstract class _$$GenderRequestImplCopyWith<$Res>
+ implements $GenderRequestCopyWith<$Res> {
+ factory _$$GenderRequestImplCopyWith(
+ _$GenderRequestImpl value,
+ $Res Function(_$GenderRequestImpl) then,
+ ) = __$$GenderRequestImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({Gender gender});
+}
+
+/// @nodoc
+class __$$GenderRequestImplCopyWithImpl<$Res>
+ extends _$GenderRequestCopyWithImpl<$Res, _$GenderRequestImpl>
+ implements _$$GenderRequestImplCopyWith<$Res> {
+ __$$GenderRequestImplCopyWithImpl(
+ _$GenderRequestImpl _value,
+ $Res Function(_$GenderRequestImpl) _then,
+ ) : super(_value, _then);
+
+ /// Create a copy of GenderRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({Object? gender = null}) {
+ return _then(
+ _$GenderRequestImpl(
+ gender: null == gender
+ ? _value.gender
+ : gender // ignore: cast_nullable_to_non_nullable
+ as Gender,
+ ),
+ );
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$GenderRequestImpl implements _GenderRequest {
+ const _$GenderRequestImpl({required this.gender});
+
+ factory _$GenderRequestImpl.fromJson(Map json) =>
+ _$$GenderRequestImplFromJson(json);
+
+ /// ์ฑ๋ณ (MALE, FEMALE, OTHER)
+ @override
+ final Gender gender;
+
+ @override
+ String toString() {
+ return 'GenderRequest(gender: $gender)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$GenderRequestImpl &&
+ (identical(other.gender, gender) || other.gender == gender));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(runtimeType, gender);
+
+ /// Create a copy of GenderRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$GenderRequestImplCopyWith<_$GenderRequestImpl> get copyWith =>
+ __$$GenderRequestImplCopyWithImpl<_$GenderRequestImpl>(this, _$identity);
+
+ @override
+ Map toJson() {
+ return _$$GenderRequestImplToJson(this);
+ }
+}
+
+abstract class _GenderRequest implements GenderRequest {
+ const factory _GenderRequest({required final Gender gender}) =
+ _$GenderRequestImpl;
+
+ factory _GenderRequest.fromJson(Map json) =
+ _$GenderRequestImpl.fromJson;
+
+ /// ์ฑ๋ณ (MALE, FEMALE, OTHER)
+ @override
+ Gender get gender;
+
+ /// Create a copy of GenderRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$GenderRequestImplCopyWith<_$GenderRequestImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/features/onboarding/data/models/gender_request.g.dart b/lib/features/onboarding/data/models/gender_request.g.dart
new file mode 100644
index 0000000..36caf86
--- /dev/null
+++ b/lib/features/onboarding/data/models/gender_request.g.dart
@@ -0,0 +1,19 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'gender_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$GenderRequestImpl _$$GenderRequestImplFromJson(Map json) =>
+ _$GenderRequestImpl(gender: $enumDecode(_$GenderEnumMap, json['gender']));
+
+Map _$$GenderRequestImplToJson(_$GenderRequestImpl instance) =>
+ {'gender': _$GenderEnumMap[instance.gender]!};
+
+const _$GenderEnumMap = {
+ Gender.male: 'MALE',
+ Gender.female: 'FEMALE',
+ Gender.other: 'OTHER',
+};
diff --git a/lib/features/onboarding/data/models/profile_request.dart b/lib/features/onboarding/data/models/profile_request.dart
new file mode 100644
index 0000000..b3256bc
--- /dev/null
+++ b/lib/features/onboarding/data/models/profile_request.dart
@@ -0,0 +1,35 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'profile_request.freezed.dart';
+part 'profile_request.g.dart';
+
+/// ํ๋กํ(๋๋ค์) ์์ฒญ ๋ชจ๋ธ
+///
+/// POST /api/members/profile ์์ฒญ ๋ฐ๋
+@freezed
+class ProfileRequest with _$ProfileRequest {
+ const factory ProfileRequest({
+ /// ๋๋ค์
+ required String name,
+ }) = _ProfileRequest;
+
+ factory ProfileRequest.fromJson(Map json) =>
+ _$ProfileRequestFromJson(json);
+}
+
+/// ๋๋ค์ ์ค๋ณต ํ์ธ ์๋ต ๋ชจ๋ธ
+///
+/// GET /api/members/check-name?name=xxx
+@freezed
+class CheckNameResponse with _$CheckNameResponse {
+ const factory CheckNameResponse({
+ /// ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ
+ required bool available,
+
+ /// ๋ฉ์์ง (์ฌ์ฉ ๋ถ๊ฐ ์ ์ด์ )
+ String? message,
+ }) = _CheckNameResponse;
+
+ factory CheckNameResponse.fromJson(Map json) =>
+ _$CheckNameResponseFromJson(json);
+}
diff --git a/lib/features/onboarding/data/models/profile_request.freezed.dart b/lib/features/onboarding/data/models/profile_request.freezed.dart
new file mode 100644
index 0000000..948aced
--- /dev/null
+++ b/lib/features/onboarding/data/models/profile_request.freezed.dart
@@ -0,0 +1,355 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'profile_request.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
+);
+
+ProfileRequest _$ProfileRequestFromJson(Map json) {
+ return _ProfileRequest.fromJson(json);
+}
+
+/// @nodoc
+mixin _$ProfileRequest {
+ /// ๋๋ค์
+ String get name => throw _privateConstructorUsedError;
+
+ /// Serializes this ProfileRequest to a JSON map.
+ Map toJson() => throw _privateConstructorUsedError;
+
+ /// Create a copy of ProfileRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $ProfileRequestCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $ProfileRequestCopyWith<$Res> {
+ factory $ProfileRequestCopyWith(
+ ProfileRequest value,
+ $Res Function(ProfileRequest) then,
+ ) = _$ProfileRequestCopyWithImpl<$Res, ProfileRequest>;
+ @useResult
+ $Res call({String name});
+}
+
+/// @nodoc
+class _$ProfileRequestCopyWithImpl<$Res, $Val extends ProfileRequest>
+ implements $ProfileRequestCopyWith<$Res> {
+ _$ProfileRequestCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of ProfileRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({Object? name = null}) {
+ return _then(
+ _value.copyWith(
+ name: null == name
+ ? _value.name
+ : name // ignore: cast_nullable_to_non_nullable
+ as String,
+ )
+ as $Val,
+ );
+ }
+}
+
+/// @nodoc
+abstract class _$$ProfileRequestImplCopyWith<$Res>
+ implements $ProfileRequestCopyWith<$Res> {
+ factory _$$ProfileRequestImplCopyWith(
+ _$ProfileRequestImpl value,
+ $Res Function(_$ProfileRequestImpl) then,
+ ) = __$$ProfileRequestImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({String name});
+}
+
+/// @nodoc
+class __$$ProfileRequestImplCopyWithImpl<$Res>
+ extends _$ProfileRequestCopyWithImpl<$Res, _$ProfileRequestImpl>
+ implements _$$ProfileRequestImplCopyWith<$Res> {
+ __$$ProfileRequestImplCopyWithImpl(
+ _$ProfileRequestImpl _value,
+ $Res Function(_$ProfileRequestImpl) _then,
+ ) : super(_value, _then);
+
+ /// Create a copy of ProfileRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({Object? name = null}) {
+ return _then(
+ _$ProfileRequestImpl(
+ name: null == name
+ ? _value.name
+ : name // ignore: cast_nullable_to_non_nullable
+ as String,
+ ),
+ );
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$ProfileRequestImpl implements _ProfileRequest {
+ const _$ProfileRequestImpl({required this.name});
+
+ factory _$ProfileRequestImpl.fromJson(Map json) =>
+ _$$ProfileRequestImplFromJson(json);
+
+ /// ๋๋ค์
+ @override
+ final String name;
+
+ @override
+ String toString() {
+ return 'ProfileRequest(name: $name)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$ProfileRequestImpl &&
+ (identical(other.name, name) || other.name == name));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(runtimeType, name);
+
+ /// Create a copy of ProfileRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$ProfileRequestImplCopyWith<_$ProfileRequestImpl> get copyWith =>
+ __$$ProfileRequestImplCopyWithImpl<_$ProfileRequestImpl>(
+ this,
+ _$identity,
+ );
+
+ @override
+ Map toJson() {
+ return _$$ProfileRequestImplToJson(this);
+ }
+}
+
+abstract class _ProfileRequest implements ProfileRequest {
+ const factory _ProfileRequest({required final String name}) =
+ _$ProfileRequestImpl;
+
+ factory _ProfileRequest.fromJson(Map json) =
+ _$ProfileRequestImpl.fromJson;
+
+ /// ๋๋ค์
+ @override
+ String get name;
+
+ /// Create a copy of ProfileRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$ProfileRequestImplCopyWith<_$ProfileRequestImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+CheckNameResponse _$CheckNameResponseFromJson(Map json) {
+ return _CheckNameResponse.fromJson(json);
+}
+
+/// @nodoc
+mixin _$CheckNameResponse {
+ /// ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ
+ bool get available => throw _privateConstructorUsedError;
+
+ /// ๋ฉ์์ง (์ฌ์ฉ ๋ถ๊ฐ ์ ์ด์ )
+ String? get message => throw _privateConstructorUsedError;
+
+ /// Serializes this CheckNameResponse to a JSON map.
+ Map toJson() => throw _privateConstructorUsedError;
+
+ /// Create a copy of CheckNameResponse
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $CheckNameResponseCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $CheckNameResponseCopyWith<$Res> {
+ factory $CheckNameResponseCopyWith(
+ CheckNameResponse value,
+ $Res Function(CheckNameResponse) then,
+ ) = _$CheckNameResponseCopyWithImpl<$Res, CheckNameResponse>;
+ @useResult
+ $Res call({bool available, String? message});
+}
+
+/// @nodoc
+class _$CheckNameResponseCopyWithImpl<$Res, $Val extends CheckNameResponse>
+ implements $CheckNameResponseCopyWith<$Res> {
+ _$CheckNameResponseCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of CheckNameResponse
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({Object? available = null, Object? message = freezed}) {
+ return _then(
+ _value.copyWith(
+ available: null == available
+ ? _value.available
+ : available // ignore: cast_nullable_to_non_nullable
+ as bool,
+ message: freezed == message
+ ? _value.message
+ : message // ignore: cast_nullable_to_non_nullable
+ as String?,
+ )
+ as $Val,
+ );
+ }
+}
+
+/// @nodoc
+abstract class _$$CheckNameResponseImplCopyWith<$Res>
+ implements $CheckNameResponseCopyWith<$Res> {
+ factory _$$CheckNameResponseImplCopyWith(
+ _$CheckNameResponseImpl value,
+ $Res Function(_$CheckNameResponseImpl) then,
+ ) = __$$CheckNameResponseImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({bool available, String? message});
+}
+
+/// @nodoc
+class __$$CheckNameResponseImplCopyWithImpl<$Res>
+ extends _$CheckNameResponseCopyWithImpl<$Res, _$CheckNameResponseImpl>
+ implements _$$CheckNameResponseImplCopyWith<$Res> {
+ __$$CheckNameResponseImplCopyWithImpl(
+ _$CheckNameResponseImpl _value,
+ $Res Function(_$CheckNameResponseImpl) _then,
+ ) : super(_value, _then);
+
+ /// Create a copy of CheckNameResponse
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({Object? available = null, Object? message = freezed}) {
+ return _then(
+ _$CheckNameResponseImpl(
+ available: null == available
+ ? _value.available
+ : available // ignore: cast_nullable_to_non_nullable
+ as bool,
+ message: freezed == message
+ ? _value.message
+ : message // ignore: cast_nullable_to_non_nullable
+ as String?,
+ ),
+ );
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$CheckNameResponseImpl implements _CheckNameResponse {
+ const _$CheckNameResponseImpl({required this.available, this.message});
+
+ factory _$CheckNameResponseImpl.fromJson(Map json) =>
+ _$$CheckNameResponseImplFromJson(json);
+
+ /// ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ
+ @override
+ final bool available;
+
+ /// ๋ฉ์์ง (์ฌ์ฉ ๋ถ๊ฐ ์ ์ด์ )
+ @override
+ final String? message;
+
+ @override
+ String toString() {
+ return 'CheckNameResponse(available: $available, message: $message)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$CheckNameResponseImpl &&
+ (identical(other.available, available) ||
+ other.available == available) &&
+ (identical(other.message, message) || other.message == message));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(runtimeType, available, message);
+
+ /// Create a copy of CheckNameResponse
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$CheckNameResponseImplCopyWith<_$CheckNameResponseImpl> get copyWith =>
+ __$$CheckNameResponseImplCopyWithImpl<_$CheckNameResponseImpl>(
+ this,
+ _$identity,
+ );
+
+ @override
+ Map toJson() {
+ return _$$CheckNameResponseImplToJson(this);
+ }
+}
+
+abstract class _CheckNameResponse implements CheckNameResponse {
+ const factory _CheckNameResponse({
+ required final bool available,
+ final String? message,
+ }) = _$CheckNameResponseImpl;
+
+ factory _CheckNameResponse.fromJson(Map json) =
+ _$CheckNameResponseImpl.fromJson;
+
+ /// ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ
+ @override
+ bool get available;
+
+ /// ๋ฉ์์ง (์ฌ์ฉ ๋ถ๊ฐ ์ ์ด์ )
+ @override
+ String? get message;
+
+ /// Create a copy of CheckNameResponse
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$CheckNameResponseImplCopyWith<_$CheckNameResponseImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/features/onboarding/data/models/profile_request.g.dart b/lib/features/onboarding/data/models/profile_request.g.dart
new file mode 100644
index 0000000..81eb341
--- /dev/null
+++ b/lib/features/onboarding/data/models/profile_request.g.dart
@@ -0,0 +1,28 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'profile_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$ProfileRequestImpl _$$ProfileRequestImplFromJson(Map json) =>
+ _$ProfileRequestImpl(name: json['name'] as String);
+
+Map _$$ProfileRequestImplToJson(
+ _$ProfileRequestImpl instance,
+) => {'name': instance.name};
+
+_$CheckNameResponseImpl _$$CheckNameResponseImplFromJson(
+ Map json,
+) => _$CheckNameResponseImpl(
+ available: json['available'] as bool,
+ message: json['message'] as String?,
+);
+
+Map _$$CheckNameResponseImplToJson(
+ _$CheckNameResponseImpl instance,
+) => {
+ 'available': instance.available,
+ 'message': instance.message,
+};
diff --git a/lib/features/onboarding/data/models/terms_request.dart b/lib/features/onboarding/data/models/terms_request.dart
new file mode 100644
index 0000000..71c7bec
--- /dev/null
+++ b/lib/features/onboarding/data/models/terms_request.dart
@@ -0,0 +1,24 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'terms_request.freezed.dart';
+part 'terms_request.g.dart';
+
+/// ์ฝ๊ด ๋์ ์์ฒญ ๋ชจ๋ธ
+///
+/// POST /api/members/onboarding/terms ์์ฒญ ๋ฐ๋
+@freezed
+class TermsRequest with _$TermsRequest {
+ const factory TermsRequest({
+ /// ์๋น์ค ์ด์ฉ์ฝ๊ด ๋์ ์ฌ๋ถ
+ required bool serviceAgreement,
+
+ /// ๊ฐ์ธ์ ๋ณด ์ฒ๋ฆฌ๋ฐฉ์นจ ๋์ ์ฌ๋ถ
+ required bool privacyAgreement,
+
+ /// ๋ง์ผํ
์ ๋ณด ์์ ๋์ ์ฌ๋ถ (์ ํ)
+ @Default(false) bool marketingAgreement,
+ }) = _TermsRequest;
+
+ factory TermsRequest.fromJson(Map json) =>
+ _$TermsRequestFromJson(json);
+}
diff --git a/lib/features/onboarding/data/models/terms_request.freezed.dart b/lib/features/onboarding/data/models/terms_request.freezed.dart
new file mode 100644
index 0000000..ff0afc6
--- /dev/null
+++ b/lib/features/onboarding/data/models/terms_request.freezed.dart
@@ -0,0 +1,243 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'terms_request.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
+);
+
+TermsRequest _$TermsRequestFromJson(Map json) {
+ return _TermsRequest.fromJson(json);
+}
+
+/// @nodoc
+mixin _$TermsRequest {
+ /// ์๋น์ค ์ด์ฉ์ฝ๊ด ๋์ ์ฌ๋ถ
+ bool get serviceAgreement => throw _privateConstructorUsedError;
+
+ /// ๊ฐ์ธ์ ๋ณด ์ฒ๋ฆฌ๋ฐฉ์นจ ๋์ ์ฌ๋ถ
+ bool get privacyAgreement => throw _privateConstructorUsedError;
+
+ /// ๋ง์ผํ
์ ๋ณด ์์ ๋์ ์ฌ๋ถ (์ ํ)
+ bool get marketingAgreement => throw _privateConstructorUsedError;
+
+ /// Serializes this TermsRequest to a JSON map.
+ Map toJson() => throw _privateConstructorUsedError;
+
+ /// Create a copy of TermsRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $TermsRequestCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $TermsRequestCopyWith<$Res> {
+ factory $TermsRequestCopyWith(
+ TermsRequest value,
+ $Res Function(TermsRequest) then,
+ ) = _$TermsRequestCopyWithImpl<$Res, TermsRequest>;
+ @useResult
+ $Res call({
+ bool serviceAgreement,
+ bool privacyAgreement,
+ bool marketingAgreement,
+ });
+}
+
+/// @nodoc
+class _$TermsRequestCopyWithImpl<$Res, $Val extends TermsRequest>
+ implements $TermsRequestCopyWith<$Res> {
+ _$TermsRequestCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of TermsRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? serviceAgreement = null,
+ Object? privacyAgreement = null,
+ Object? marketingAgreement = null,
+ }) {
+ return _then(
+ _value.copyWith(
+ serviceAgreement: null == serviceAgreement
+ ? _value.serviceAgreement
+ : serviceAgreement // ignore: cast_nullable_to_non_nullable
+ as bool,
+ privacyAgreement: null == privacyAgreement
+ ? _value.privacyAgreement
+ : privacyAgreement // ignore: cast_nullable_to_non_nullable
+ as bool,
+ marketingAgreement: null == marketingAgreement
+ ? _value.marketingAgreement
+ : marketingAgreement // ignore: cast_nullable_to_non_nullable
+ as bool,
+ )
+ as $Val,
+ );
+ }
+}
+
+/// @nodoc
+abstract class _$$TermsRequestImplCopyWith<$Res>
+ implements $TermsRequestCopyWith<$Res> {
+ factory _$$TermsRequestImplCopyWith(
+ _$TermsRequestImpl value,
+ $Res Function(_$TermsRequestImpl) then,
+ ) = __$$TermsRequestImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({
+ bool serviceAgreement,
+ bool privacyAgreement,
+ bool marketingAgreement,
+ });
+}
+
+/// @nodoc
+class __$$TermsRequestImplCopyWithImpl<$Res>
+ extends _$TermsRequestCopyWithImpl<$Res, _$TermsRequestImpl>
+ implements _$$TermsRequestImplCopyWith<$Res> {
+ __$$TermsRequestImplCopyWithImpl(
+ _$TermsRequestImpl _value,
+ $Res Function(_$TermsRequestImpl) _then,
+ ) : super(_value, _then);
+
+ /// Create a copy of TermsRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? serviceAgreement = null,
+ Object? privacyAgreement = null,
+ Object? marketingAgreement = null,
+ }) {
+ return _then(
+ _$TermsRequestImpl(
+ serviceAgreement: null == serviceAgreement
+ ? _value.serviceAgreement
+ : serviceAgreement // ignore: cast_nullable_to_non_nullable
+ as bool,
+ privacyAgreement: null == privacyAgreement
+ ? _value.privacyAgreement
+ : privacyAgreement // ignore: cast_nullable_to_non_nullable
+ as bool,
+ marketingAgreement: null == marketingAgreement
+ ? _value.marketingAgreement
+ : marketingAgreement // ignore: cast_nullable_to_non_nullable
+ as bool,
+ ),
+ );
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$TermsRequestImpl implements _TermsRequest {
+ const _$TermsRequestImpl({
+ required this.serviceAgreement,
+ required this.privacyAgreement,
+ this.marketingAgreement = false,
+ });
+
+ factory _$TermsRequestImpl.fromJson(Map json) =>
+ _$$TermsRequestImplFromJson(json);
+
+ /// ์๋น์ค ์ด์ฉ์ฝ๊ด ๋์ ์ฌ๋ถ
+ @override
+ final bool serviceAgreement;
+
+ /// ๊ฐ์ธ์ ๋ณด ์ฒ๋ฆฌ๋ฐฉ์นจ ๋์ ์ฌ๋ถ
+ @override
+ final bool privacyAgreement;
+
+ /// ๋ง์ผํ
์ ๋ณด ์์ ๋์ ์ฌ๋ถ (์ ํ)
+ @override
+ @JsonKey()
+ final bool marketingAgreement;
+
+ @override
+ String toString() {
+ return 'TermsRequest(serviceAgreement: $serviceAgreement, privacyAgreement: $privacyAgreement, marketingAgreement: $marketingAgreement)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$TermsRequestImpl &&
+ (identical(other.serviceAgreement, serviceAgreement) ||
+ other.serviceAgreement == serviceAgreement) &&
+ (identical(other.privacyAgreement, privacyAgreement) ||
+ other.privacyAgreement == privacyAgreement) &&
+ (identical(other.marketingAgreement, marketingAgreement) ||
+ other.marketingAgreement == marketingAgreement));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(
+ runtimeType,
+ serviceAgreement,
+ privacyAgreement,
+ marketingAgreement,
+ );
+
+ /// Create a copy of TermsRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$TermsRequestImplCopyWith<_$TermsRequestImpl> get copyWith =>
+ __$$TermsRequestImplCopyWithImpl<_$TermsRequestImpl>(this, _$identity);
+
+ @override
+ Map toJson() {
+ return _$$TermsRequestImplToJson(this);
+ }
+}
+
+abstract class _TermsRequest implements TermsRequest {
+ const factory _TermsRequest({
+ required final bool serviceAgreement,
+ required final bool privacyAgreement,
+ final bool marketingAgreement,
+ }) = _$TermsRequestImpl;
+
+ factory _TermsRequest.fromJson(Map json) =
+ _$TermsRequestImpl.fromJson;
+
+ /// ์๋น์ค ์ด์ฉ์ฝ๊ด ๋์ ์ฌ๋ถ
+ @override
+ bool get serviceAgreement;
+
+ /// ๊ฐ์ธ์ ๋ณด ์ฒ๋ฆฌ๋ฐฉ์นจ ๋์ ์ฌ๋ถ
+ @override
+ bool get privacyAgreement;
+
+ /// ๋ง์ผํ
์ ๋ณด ์์ ๋์ ์ฌ๋ถ (์ ํ)
+ @override
+ bool get marketingAgreement;
+
+ /// Create a copy of TermsRequest
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$TermsRequestImplCopyWith<_$TermsRequestImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/features/onboarding/data/models/terms_request.g.dart b/lib/features/onboarding/data/models/terms_request.g.dart
new file mode 100644
index 0000000..cd29b91
--- /dev/null
+++ b/lib/features/onboarding/data/models/terms_request.g.dart
@@ -0,0 +1,21 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'terms_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$TermsRequestImpl _$$TermsRequestImplFromJson(Map json) =>
+ _$TermsRequestImpl(
+ serviceAgreement: json['serviceAgreement'] as bool,
+ privacyAgreement: json['privacyAgreement'] as bool,
+ marketingAgreement: json['marketingAgreement'] as bool? ?? false,
+ );
+
+Map _$$TermsRequestImplToJson(_$TermsRequestImpl instance) =>
+ {
+ 'serviceAgreement': instance.serviceAgreement,
+ 'privacyAgreement': instance.privacyAgreement,
+ 'marketingAgreement': instance.marketingAgreement,
+ };
diff --git a/lib/features/onboarding/data/repositories/onboarding_repository_impl.dart b/lib/features/onboarding/data/repositories/onboarding_repository_impl.dart
new file mode 100644
index 0000000..dbe8043
--- /dev/null
+++ b/lib/features/onboarding/data/repositories/onboarding_repository_impl.dart
@@ -0,0 +1,122 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../../../../core/network/token_storage.dart';
+import '../../domain/repositories/onboarding_repository.dart';
+import '../datasources/onboarding_remote_datasource.dart';
+import '../models/terms_request.dart';
+import '../models/birth_date_request.dart';
+import '../models/gender_request.dart';
+import '../models/profile_request.dart';
+
+part 'onboarding_repository_impl.g.dart';
+
+/// OnboardingRepository Provider
+@riverpod
+OnboardingRepository onboardingRepository(Ref ref) {
+ final remoteDataSource = ref.watch(onboardingRemoteDataSourceProvider);
+ final tokenStorage = ref.watch(tokenStorageProvider);
+ return OnboardingRepositoryImpl(remoteDataSource, tokenStorage);
+}
+
+/// ์จ๋ณด๋ฉ Repository ๊ตฌํ์ฒด
+class OnboardingRepositoryImpl implements OnboardingRepository {
+ final OnboardingRemoteDataSource _remoteDataSource;
+ final TokenStorage _tokenStorage;
+
+ OnboardingRepositoryImpl(this._remoteDataSource, this._tokenStorage);
+
+ @override
+ Future submitTerms({
+ required bool serviceAgreement,
+ required bool privacyAgreement,
+ bool marketingAgreement = false,
+ }) async {
+ debugPrint('๐ OnboardingRepository: Submitting terms...');
+
+ final request = TermsRequest(
+ serviceAgreement: serviceAgreement,
+ privacyAgreement: privacyAgreement,
+ marketingAgreement: marketingAgreement,
+ );
+
+ await _remoteDataSource.submitTerms(request);
+
+ // ์จ๋ณด๋ฉ ๋จ๊ณ ์
๋ฐ์ดํธ: TERMS โ BIRTH_DATE
+ await _tokenStorage.saveOnboardingState(
+ requiresOnboarding: true,
+ onboardingStep: 'BIRTH_DATE',
+ );
+
+ debugPrint('โ
Terms submitted, next step: BIRTH_DATE');
+ }
+
+ @override
+ Future submitBirthDate(DateTime birthDate) async {
+ debugPrint('๐ OnboardingRepository: Submitting birth date...');
+
+ // DateTime์ YYYY-MM-DD ํ์์ผ๋ก ๋ณํ
+ final year = birthDate.year.toString();
+ final month = birthDate.month.toString().padLeft(2, '0');
+ final day = birthDate.day.toString().padLeft(2, '0');
+ final formattedDate = '$year-$month-$day';
+
+ final request = BirthDateRequest(birthDate: formattedDate);
+ await _remoteDataSource.submitBirthDate(request);
+
+ // ์จ๋ณด๋ฉ ๋จ๊ณ ์
๋ฐ์ดํธ: BIRTH_DATE โ GENDER
+ await _tokenStorage.saveOnboardingState(
+ requiresOnboarding: true,
+ onboardingStep: 'GENDER',
+ );
+
+ debugPrint('โ
Birth date submitted, next step: GENDER');
+ }
+
+ @override
+ Future submitGender(Gender gender) async {
+ debugPrint('๐ OnboardingRepository: Submitting gender...');
+
+ final request = GenderRequest(gender: gender);
+ await _remoteDataSource.submitGender(request);
+
+ // ์จ๋ณด๋ฉ ๋จ๊ณ ์
๋ฐ์ดํธ: GENDER โ NICKNAME
+ await _tokenStorage.saveOnboardingState(
+ requiresOnboarding: true,
+ onboardingStep: 'NICKNAME',
+ );
+
+ debugPrint('โ
Gender submitted, next step: NICKNAME');
+ }
+
+ @override
+ Future submitProfile(String name) async {
+ debugPrint('๐ OnboardingRepository: Submitting profile...');
+
+ final request = ProfileRequest(name: name);
+ await _remoteDataSource.submitProfile(request);
+
+ // ์จ๋ณด๋ฉ ์๋ฃ
+ await completeOnboarding();
+
+ debugPrint('โ
Profile submitted, onboarding completed');
+ }
+
+ @override
+ Future checkName(String name) async {
+ return await _remoteDataSource.checkName(name);
+ }
+
+ @override
+ Future completeOnboarding() async {
+ debugPrint('๐ OnboardingRepository: Completing onboarding...');
+
+ await _tokenStorage.saveOnboardingState(
+ requiresOnboarding: false,
+ onboardingStep: 'COMPLETED',
+ );
+
+ debugPrint('โ
Onboarding completed');
+ }
+}
diff --git a/lib/features/onboarding/data/repositories/onboarding_repository_impl.g.dart b/lib/features/onboarding/data/repositories/onboarding_repository_impl.g.dart
new file mode 100644
index 0000000..e5c6525
--- /dev/null
+++ b/lib/features/onboarding/data/repositories/onboarding_repository_impl.g.dart
@@ -0,0 +1,31 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'onboarding_repository_impl.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$onboardingRepositoryHash() =>
+ r'f783674177423cd59773cf45726df55d0e0c4e67';
+
+/// OnboardingRepository Provider
+///
+/// Copied from [onboardingRepository].
+@ProviderFor(onboardingRepository)
+final onboardingRepositoryProvider =
+ AutoDisposeProvider.internal(
+ onboardingRepository,
+ name: r'onboardingRepositoryProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$onboardingRepositoryHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+ );
+
+@Deprecated('Will be removed in 3.0. Use Ref instead')
+// ignore: unused_element
+typedef OnboardingRepositoryRef = AutoDisposeProviderRef;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
diff --git a/lib/features/onboarding/domain/repositories/onboarding_repository.dart b/lib/features/onboarding/domain/repositories/onboarding_repository.dart
new file mode 100644
index 0000000..8d03845
--- /dev/null
+++ b/lib/features/onboarding/domain/repositories/onboarding_repository.dart
@@ -0,0 +1,41 @@
+import '../../data/models/gender_request.dart';
+import '../../data/models/profile_request.dart';
+
+/// ์จ๋ณด๋ฉ Repository ์ธํฐํ์ด์ค
+abstract class OnboardingRepository {
+ /// ์ฝ๊ด ๋์ ์ ์ถ
+ ///
+ /// [serviceAgreement]: ์๋น์ค ์ด์ฉ์ฝ๊ด ๋์
+ /// [privacyAgreement]: ๊ฐ์ธ์ ๋ณด ์ฒ๋ฆฌ๋ฐฉ์นจ ๋์
+ /// [marketingAgreement]: ๋ง์ผํ
์ ๋ณด ์์ ๋์ (์ ํ)
+ Future submitTerms({
+ required bool serviceAgreement,
+ required bool privacyAgreement,
+ bool marketingAgreement = false,
+ });
+
+ /// ์๋
์์ผ ์ ์ถ
+ ///
+ /// [birthDate]: ์๋
์์ผ (DateTime)
+ Future submitBirthDate(DateTime birthDate);
+
+ /// ์ฑ๋ณ ์ ์ถ
+ ///
+ /// [gender]: ์ฑ๋ณ (Gender enum)
+ Future submitGender(Gender gender);
+
+ /// ํ๋กํ(๋๋ค์) ์ ์ถ
+ ///
+ /// [name]: ๋๋ค์
+ Future submitProfile(String name);
+
+ /// ๋๋ค์ ์ค๋ณต ํ์ธ
+ ///
+ /// Returns: ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ์ ๋ฉ์์ง
+ Future checkName(String name);
+
+ /// ์จ๋ณด๋ฉ ์๋ฃ ์ฒ๋ฆฌ
+ ///
+ /// ๋ก์ปฌ ์ ์ฅ์์ ์จ๋ณด๋ฉ ์ํ๋ฅผ ์๋ฃ๋ก ๋ณ๊ฒฝ
+ Future completeOnboarding();
+}
diff --git a/lib/features/onboarding/presentation/pages/birth_date_step_page.dart b/lib/features/onboarding/presentation/pages/birth_date_step_page.dart
new file mode 100644
index 0000000..a41e9c6
--- /dev/null
+++ b/lib/features/onboarding/presentation/pages/birth_date_step_page.dart
@@ -0,0 +1,276 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:go_router/go_router.dart';
+
+import '../../../../core/constants/app_colors.dart';
+import '../../../../router/route_paths.dart';
+import '../providers/onboarding_provider.dart';
+import '../widgets/onboarding_button.dart';
+import '../widgets/step_indicator.dart';
+
+/// ์๋
์์ผ ์
๋ ฅ ํ๋ฉด
+class BirthDateStepPage extends ConsumerStatefulWidget {
+ const BirthDateStepPage({super.key});
+
+ @override
+ ConsumerState createState() => _BirthDateStepPageState();
+}
+
+class _BirthDateStepPageState extends ConsumerState {
+ late DateTime _selectedDate;
+
+ @override
+ void initState() {
+ super.initState();
+ // ๊ธฐ๋ณธ๊ฐ: 20๋
์
+ _selectedDate = DateTime.now().subtract(const Duration(days: 365 * 20));
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final state = ref.watch(onboardingNotifierProvider);
+ final notifier = ref.read(onboardingNotifierProvider.notifier);
+
+ return Scaffold(
+ backgroundColor: Colors.white,
+ appBar: AppBar(
+ backgroundColor: Colors.white,
+ elevation: 0,
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back_ios, color: AppColors.gray700),
+ onPressed: () => context.go(RoutePaths.onboardingTerms),
+ ),
+ ),
+ body: SafeArea(
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 24.w),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // ์งํ ํ์๊ธฐ
+ const StepIndicator(currentStep: OnboardingStep.birthDate),
+ SizedBox(height: 32.h),
+
+ // ํ์ดํ
+ Text(
+ '์๋
์์ผ์\n์๋ ค์ฃผ์ธ์',
+ style: TextStyle(
+ fontSize: 24.sp,
+ fontWeight: FontWeight.w700,
+ color: AppColors.gray900,
+ height: 1.3,
+ ),
+ ),
+ SizedBox(height: 8.h),
+ Text(
+ '๋ง์ถค ์ฝํ
์ธ ์ถ์ฒ์ ์ํด ์ฌ์ฉ๋ฉ๋๋ค.',
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: AppColors.gray600,
+ ),
+ ),
+ SizedBox(height: 48.h),
+
+ // ๋ ์ง ์ ํ ํ์
+ _DateDisplay(
+ date: state.birthDate ?? _selectedDate,
+ onTap: () => _showDatePicker(context, notifier),
+ ),
+ SizedBox(height: 24.h),
+
+ // ๋ ์ง ์ ํ ๋ฒํผ
+ OutlinedButton.icon(
+ onPressed: () => _showDatePicker(context, notifier),
+ icon: const Icon(Icons.calendar_today),
+ label: const Text('๋ ์ง ์ ํํ๊ธฐ'),
+ style: OutlinedButton.styleFrom(
+ foregroundColor: AppColors.primary,
+ side: const BorderSide(color: AppColors.primary),
+ padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 12.h),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8.r),
+ ),
+ ),
+ ),
+
+ const Spacer(),
+
+ // ์๋ฌ ๋ฉ์์ง
+ if (state.errorMessage != null) ...[
+ Text(
+ state.errorMessage!,
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: AppColors.error,
+ ),
+ ),
+ SizedBox(height: 8.h),
+ ],
+
+ // ๋ค์ ๋ฒํผ
+ OnboardingButton(
+ text: '๋ค์',
+ isLoading: state.isLoading,
+ onPressed: state.birthDate != null
+ ? () async {
+ final success = await notifier.submitBirthDate();
+ if (success && context.mounted) {
+ context.go(RoutePaths.onboardingGender);
+ }
+ }
+ : null,
+ ),
+ SizedBox(height: 32.h),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ void _showDatePicker(BuildContext context, OnboardingNotifier notifier) {
+ final now = DateTime.now();
+ final minDate = DateTime(now.year - 100);
+ final maxDate = DateTime(now.year - 10); // ์ต์ 10์ธ ์ด์
+
+ showCupertinoModalPopup(
+ context: context,
+ builder: (context) => Container(
+ height: 300.h,
+ color: Colors.white,
+ child: Column(
+ children: [
+ Container(
+ height: 50.h,
+ padding: EdgeInsets.symmetric(horizontal: 16.w),
+ decoration: const BoxDecoration(
+ color: AppColors.gray100,
+ border: Border(
+ bottom: BorderSide(color: AppColors.gray200),
+ ),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ CupertinoButton(
+ padding: EdgeInsets.zero,
+ child: Text(
+ '์ทจ์',
+ style: TextStyle(
+ fontSize: 16.sp,
+ color: AppColors.gray600,
+ ),
+ ),
+ onPressed: () => Navigator.pop(context),
+ ),
+ CupertinoButton(
+ padding: EdgeInsets.zero,
+ child: Text(
+ 'ํ์ธ',
+ style: TextStyle(
+ fontSize: 16.sp,
+ color: AppColors.primary,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ onPressed: () {
+ notifier.setBirthDate(_selectedDate);
+ Navigator.pop(context);
+ },
+ ),
+ ],
+ ),
+ ),
+ Expanded(
+ child: CupertinoDatePicker(
+ mode: CupertinoDatePickerMode.date,
+ initialDateTime: _selectedDate,
+ minimumDate: minDate,
+ maximumDate: maxDate,
+ onDateTimeChanged: (date) {
+ setState(() {
+ _selectedDate = date;
+ });
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+/// ๋ ์ง ํ์ ์์ ฏ
+class _DateDisplay extends StatelessWidget {
+ final DateTime date;
+ final VoidCallback onTap;
+
+ const _DateDisplay({
+ required this.date,
+ required this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return InkWell(
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(12.r),
+ child: Container(
+ width: double.infinity,
+ padding: EdgeInsets.all(20.w),
+ decoration: BoxDecoration(
+ color: AppColors.gray100,
+ borderRadius: BorderRadius.circular(12.r),
+ border: Border.all(color: AppColors.gray200),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ _DateBox(value: date.year.toString(), label: '๋
'),
+ SizedBox(width: 16.w),
+ _DateBox(value: date.month.toString().padLeft(2, '0'), label: '์'),
+ SizedBox(width: 16.w),
+ _DateBox(value: date.day.toString().padLeft(2, '0'), label: '์ผ'),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _DateBox extends StatelessWidget {
+ final String value;
+ final String label;
+
+ const _DateBox({
+ required this.value,
+ required this.label,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ Text(
+ value,
+ style: TextStyle(
+ fontSize: 28.sp,
+ fontWeight: FontWeight.w700,
+ color: AppColors.gray900,
+ ),
+ ),
+ SizedBox(height: 4.h),
+ Text(
+ label,
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: AppColors.gray500,
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/features/onboarding/presentation/pages/gender_step_page.dart b/lib/features/onboarding/presentation/pages/gender_step_page.dart
new file mode 100644
index 0000000..e774df0
--- /dev/null
+++ b/lib/features/onboarding/presentation/pages/gender_step_page.dart
@@ -0,0 +1,179 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:go_router/go_router.dart';
+
+import '../../../../core/constants/app_colors.dart';
+import '../../../../router/route_paths.dart';
+import '../../data/models/gender_request.dart';
+import '../providers/onboarding_provider.dart';
+import '../widgets/onboarding_button.dart';
+import '../widgets/step_indicator.dart';
+
+/// ์ฑ๋ณ ์ ํ ํ๋ฉด
+class GenderStepPage extends ConsumerWidget {
+ const GenderStepPage({super.key});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final state = ref.watch(onboardingNotifierProvider);
+ final notifier = ref.read(onboardingNotifierProvider.notifier);
+
+ return Scaffold(
+ backgroundColor: Colors.white,
+ appBar: AppBar(
+ backgroundColor: Colors.white,
+ elevation: 0,
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back_ios, color: AppColors.gray700),
+ onPressed: () => context.go(RoutePaths.onboardingBirthDate),
+ ),
+ ),
+ body: SafeArea(
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 24.w),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // ์งํ ํ์๊ธฐ
+ const StepIndicator(currentStep: OnboardingStep.gender),
+ SizedBox(height: 32.h),
+
+ // ํ์ดํ
+ Text(
+ '์ฑ๋ณ์\n์๋ ค์ฃผ์ธ์',
+ style: TextStyle(
+ fontSize: 24.sp,
+ fontWeight: FontWeight.w700,
+ color: AppColors.gray900,
+ height: 1.3,
+ ),
+ ),
+ SizedBox(height: 8.h),
+ Text(
+ '๋ง์ถค ์ฝํ
์ธ ์ถ์ฒ์ ์ํด ์ฌ์ฉ๋ฉ๋๋ค.',
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: AppColors.gray600,
+ ),
+ ),
+ SizedBox(height: 48.h),
+
+ // ์ฑ๋ณ ์ ํ ๋ฒํผ๋ค
+ Row(
+ children: [
+ Expanded(
+ child: _GenderButton(
+ label: '๋จ์ฑ',
+ icon: Icons.male,
+ isSelected: state.gender == Gender.male,
+ onTap: () => notifier.setGender(Gender.male),
+ ),
+ ),
+ SizedBox(width: 12.w),
+ Expanded(
+ child: _GenderButton(
+ label: '์ฌ์ฑ',
+ icon: Icons.female,
+ isSelected: state.gender == Gender.female,
+ onTap: () => notifier.setGender(Gender.female),
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 12.h),
+ _GenderButton(
+ label: '๊ธฐํ',
+ icon: Icons.person_outline,
+ isSelected: state.gender == Gender.other,
+ onTap: () => notifier.setGender(Gender.other),
+ ),
+
+ const Spacer(),
+
+ // ์๋ฌ ๋ฉ์์ง
+ if (state.errorMessage != null) ...[
+ Text(
+ state.errorMessage!,
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: AppColors.error,
+ ),
+ ),
+ SizedBox(height: 8.h),
+ ],
+
+ // ๋ค์ ๋ฒํผ
+ OnboardingButton(
+ text: '๋ค์',
+ isLoading: state.isLoading,
+ onPressed: state.gender != null
+ ? () async {
+ final success = await notifier.submitGender();
+ if (success && context.mounted) {
+ context.go(RoutePaths.onboardingNickname);
+ }
+ }
+ : null,
+ ),
+ SizedBox(height: 32.h),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+/// ์ฑ๋ณ ์ ํ ๋ฒํผ
+class _GenderButton extends StatelessWidget {
+ final String label;
+ final IconData icon;
+ final bool isSelected;
+ final VoidCallback onTap;
+
+ const _GenderButton({
+ required this.label,
+ required this.icon,
+ required this.isSelected,
+ required this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return InkWell(
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(12.r),
+ child: Container(
+ padding: EdgeInsets.symmetric(vertical: 24.h),
+ decoration: BoxDecoration(
+ color: isSelected ? AppColors.primary.withValues(alpha: 0.1) : AppColors.gray100,
+ borderRadius: BorderRadius.circular(12.r),
+ border: Border.all(
+ color: isSelected ? AppColors.primary : AppColors.gray200,
+ width: isSelected ? 2 : 1,
+ ),
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ icon,
+ size: 48.w,
+ color: isSelected ? AppColors.primary : AppColors.gray500,
+ ),
+ SizedBox(height: 12.h),
+ Text(
+ label,
+ style: TextStyle(
+ fontSize: 16.sp,
+ fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
+ color: isSelected ? AppColors.primary : AppColors.gray700,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/onboarding/presentation/pages/nickname_step_page.dart b/lib/features/onboarding/presentation/pages/nickname_step_page.dart
new file mode 100644
index 0000000..c3bbb8e
--- /dev/null
+++ b/lib/features/onboarding/presentation/pages/nickname_step_page.dart
@@ -0,0 +1,298 @@
+import 'dart:async';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:go_router/go_router.dart';
+
+import '../../../../core/constants/app_colors.dart';
+import '../../../../router/route_paths.dart';
+import '../providers/onboarding_provider.dart';
+import '../widgets/onboarding_button.dart';
+import '../widgets/step_indicator.dart';
+
+/// ๋๋ค์ ์
๋ ฅ ํ๋ฉด
+class NicknameStepPage extends ConsumerStatefulWidget {
+ const NicknameStepPage({super.key});
+
+ @override
+ ConsumerState createState() => _NicknameStepPageState();
+}
+
+class _NicknameStepPageState extends ConsumerState {
+ final _controller = TextEditingController();
+ final _focusNode = FocusNode();
+ Timer? _debounce;
+
+ @override
+ void initState() {
+ super.initState();
+ _focusNode.requestFocus();
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ _focusNode.dispose();
+ _debounce?.cancel();
+ super.dispose();
+ }
+
+ void _onNicknameChanged(String value, OnboardingNotifier notifier) {
+ notifier.setNickname(value);
+
+ // ๋๋ฐ์ด์ค: ์
๋ ฅ ํ 500ms ํ์ ์ค๋ณต ํ์ธ
+ _debounce?.cancel();
+ if (value.length >= 2) {
+ _debounce = Timer(const Duration(milliseconds: 500), () {
+ notifier.checkNickname();
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final state = ref.watch(onboardingNotifierProvider);
+ final notifier = ref.read(onboardingNotifierProvider.notifier);
+
+ return Scaffold(
+ backgroundColor: Colors.white,
+ appBar: AppBar(
+ backgroundColor: Colors.white,
+ elevation: 0,
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back_ios, color: AppColors.gray700),
+ onPressed: () => context.go(RoutePaths.onboardingGender),
+ ),
+ ),
+ body: SafeArea(
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 24.w),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // ์งํ ํ์๊ธฐ
+ const StepIndicator(currentStep: OnboardingStep.nickname),
+ SizedBox(height: 32.h),
+
+ // ํ์ดํ
+ Text(
+ '๋๋ค์์\n์ค์ ํด์ฃผ์ธ์',
+ style: TextStyle(
+ fontSize: 24.sp,
+ fontWeight: FontWeight.w700,
+ color: AppColors.gray900,
+ height: 1.3,
+ ),
+ ),
+ SizedBox(height: 8.h),
+ Text(
+ '๋ค๋ฅธ ์ฌ์ฉ์์๊ฒ ํ์๋๋ ์ด๋ฆ์
๋๋ค.',
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: AppColors.gray600,
+ ),
+ ),
+ SizedBox(height: 48.h),
+
+ // ๋๋ค์ ์
๋ ฅ ํ๋
+ TextField(
+ controller: _controller,
+ focusNode: _focusNode,
+ onChanged: (value) => _onNicknameChanged(value, notifier),
+ maxLength: 20,
+ style: TextStyle(
+ fontSize: 16.sp,
+ color: AppColors.gray900,
+ ),
+ decoration: InputDecoration(
+ hintText: '๋๋ค์ ์
๋ ฅ (2-20์)',
+ hintStyle: TextStyle(
+ color: AppColors.gray400,
+ fontSize: 16.sp,
+ ),
+ filled: true,
+ fillColor: AppColors.gray100,
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(12.r),
+ borderSide: BorderSide.none,
+ ),
+ enabledBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(12.r),
+ borderSide: BorderSide(
+ color: _getBorderColor(state),
+ ),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(12.r),
+ borderSide: BorderSide(
+ color: _getBorderColor(state),
+ width: 2,
+ ),
+ ),
+ contentPadding: EdgeInsets.all(16.w),
+ counterText: '',
+ suffixIcon: _buildSuffixIcon(state),
+ ),
+ ),
+ SizedBox(height: 8.h),
+
+ // ์ํ ๋ฉ์์ง
+ _buildStatusMessage(state),
+
+ SizedBox(height: 16.h),
+
+ // ๋๋ค์ ๊ท์น ์๋ด
+ _NicknameRules(),
+
+ const Spacer(),
+
+ // ์๋ฃ ๋ฒํผ
+ OnboardingButton(
+ text: '์๋ฃ',
+ isLoading: state.isLoading,
+ onPressed: notifier.canSubmitNickname
+ ? () async {
+ final success = await notifier.submitProfile();
+ if (success && context.mounted) {
+ context.go(RoutePaths.home);
+ }
+ }
+ : null,
+ ),
+ SizedBox(height: 32.h),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Color _getBorderColor(OnboardingState state) {
+ if (state.nicknameAvailable == true) {
+ return AppColors.success;
+ } else if (state.nicknameAvailable == false) {
+ return AppColors.error;
+ }
+ return AppColors.gray200;
+ }
+
+ Widget? _buildSuffixIcon(OnboardingState state) {
+ if (state.isLoading) {
+ return Padding(
+ padding: EdgeInsets.all(12.w),
+ child: SizedBox(
+ width: 20.w,
+ height: 20.h,
+ child: const CircularProgressIndicator(
+ strokeWidth: 2,
+ color: AppColors.primary,
+ ),
+ ),
+ );
+ }
+
+ if (state.nicknameAvailable == true) {
+ return Icon(
+ Icons.check_circle,
+ color: AppColors.success,
+ size: 24.w,
+ );
+ }
+
+ if (state.nicknameAvailable == false) {
+ return Icon(
+ Icons.error,
+ color: AppColors.error,
+ size: 24.w,
+ );
+ }
+
+ return null;
+ }
+
+ Widget _buildStatusMessage(OnboardingState state) {
+ if (state.nicknameAvailable == true) {
+ return Text(
+ '์ฌ์ฉ ๊ฐ๋ฅํ ๋๋ค์์
๋๋ค.',
+ style: TextStyle(
+ fontSize: 13.sp,
+ color: AppColors.success,
+ ),
+ );
+ }
+
+ if (state.errorMessage != null) {
+ return Text(
+ state.errorMessage!,
+ style: TextStyle(
+ fontSize: 13.sp,
+ color: AppColors.error,
+ ),
+ );
+ }
+
+ return const SizedBox.shrink();
+ }
+}
+
+/// ๋๋ค์ ๊ท์น ์๋ด
+class _NicknameRules extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: EdgeInsets.all(16.w),
+ decoration: BoxDecoration(
+ color: AppColors.gray50,
+ borderRadius: BorderRadius.circular(8.r),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '๋๋ค์ ๊ท์น',
+ style: TextStyle(
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w600,
+ color: AppColors.gray700,
+ ),
+ ),
+ SizedBox(height: 8.h),
+ _RuleItem(text: '2~20์ ์ด๋ด'),
+ _RuleItem(text: 'ํ๊ธ, ์๋ฌธ, ์ซ์ ์ฌ์ฉ ๊ฐ๋ฅ'),
+ _RuleItem(text: 'ํน์๋ฌธ์ ์ฌ์ฉ ๋ถ๊ฐ'),
+ _RuleItem(text: '๊ณต๋ฐฑ ์ฌ์ฉ ๋ถ๊ฐ'),
+ ],
+ ),
+ );
+ }
+}
+
+class _RuleItem extends StatelessWidget {
+ final String text;
+
+ const _RuleItem({required this.text});
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: EdgeInsets.only(top: 4.h),
+ child: Row(
+ children: [
+ Icon(
+ Icons.check,
+ size: 14.w,
+ color: AppColors.gray500,
+ ),
+ SizedBox(width: 6.w),
+ Text(
+ text,
+ style: TextStyle(
+ fontSize: 13.sp,
+ color: AppColors.gray600,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/features/onboarding/presentation/pages/terms_step_page.dart b/lib/features/onboarding/presentation/pages/terms_step_page.dart
new file mode 100644
index 0000000..c089466
--- /dev/null
+++ b/lib/features/onboarding/presentation/pages/terms_step_page.dart
@@ -0,0 +1,289 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:go_router/go_router.dart';
+
+import '../../../../core/constants/app_colors.dart';
+import '../../../../router/route_paths.dart';
+import '../providers/onboarding_provider.dart';
+import '../widgets/onboarding_button.dart';
+import '../widgets/step_indicator.dart';
+
+/// ์ฝ๊ด ๋์ ํ๋ฉด
+class TermsStepPage extends ConsumerWidget {
+ const TermsStepPage({super.key});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final state = ref.watch(onboardingNotifierProvider);
+ final notifier = ref.read(onboardingNotifierProvider.notifier);
+
+ return Scaffold(
+ backgroundColor: Colors.white,
+ appBar: AppBar(
+ backgroundColor: Colors.white,
+ elevation: 0,
+ automaticallyImplyLeading: false,
+ ),
+ body: SafeArea(
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 24.w),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // ์งํ ํ์๊ธฐ
+ const StepIndicator(currentStep: OnboardingStep.terms),
+ SizedBox(height: 32.h),
+
+ // ํ์ดํ
+ Text(
+ '์๋น์ค ์ด์ฉ์ ์ํด\n์ฝ๊ด์ ๋์ํด์ฃผ์ธ์',
+ style: TextStyle(
+ fontSize: 24.sp,
+ fontWeight: FontWeight.w700,
+ color: AppColors.gray900,
+ height: 1.3,
+ ),
+ ),
+ SizedBox(height: 8.h),
+ Text(
+ '์ํํ ์๋น์ค ์ด์ฉ์ ์ํด ํ์ ์ฝ๊ด์ ๋์๊ฐ ํ์ํฉ๋๋ค.',
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: AppColors.gray600,
+ ),
+ ),
+ SizedBox(height: 32.h),
+
+ // ์ ์ฒด ๋์
+ _AllAgreeItem(
+ isChecked: state.serviceAgreed && state.privacyAgreed && state.marketingAgreed,
+ onTap: notifier.agreeAll,
+ ),
+ SizedBox(height: 16.h),
+ const Divider(color: AppColors.gray200),
+ SizedBox(height: 16.h),
+
+ // ๊ฐ๋ณ ์ฝ๊ด
+ _AgreementItem(
+ title: '[ํ์] ์๋น์ค ์ด์ฉ์ฝ๊ด',
+ isChecked: state.serviceAgreed,
+ onTap: notifier.toggleServiceAgreement,
+ onDetailTap: () => _showTermsDetail(context, '์๋น์ค ์ด์ฉ์ฝ๊ด'),
+ ),
+ SizedBox(height: 12.h),
+ _AgreementItem(
+ title: '[ํ์] ๊ฐ์ธ์ ๋ณด ์ฒ๋ฆฌ๋ฐฉ์นจ',
+ isChecked: state.privacyAgreed,
+ onTap: notifier.togglePrivacyAgreement,
+ onDetailTap: () => _showTermsDetail(context, '๊ฐ์ธ์ ๋ณด ์ฒ๋ฆฌ๋ฐฉ์นจ'),
+ ),
+ SizedBox(height: 12.h),
+ _AgreementItem(
+ title: '[์ ํ] ๋ง์ผํ
์ ๋ณด ์์ ๋์',
+ isChecked: state.marketingAgreed,
+ onTap: notifier.toggleMarketingAgreement,
+ onDetailTap: () => _showTermsDetail(context, '๋ง์ผํ
์ ๋ณด ์์ '),
+ ),
+
+ const Spacer(),
+
+ // ์๋ฌ ๋ฉ์์ง
+ if (state.errorMessage != null) ...[
+ Text(
+ state.errorMessage!,
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: AppColors.error,
+ ),
+ ),
+ SizedBox(height: 8.h),
+ ],
+
+ // ๋ค์ ๋ฒํผ
+ OnboardingButton(
+ text: '๋์ํ๊ณ ๊ณ์ํ๊ธฐ',
+ isLoading: state.isLoading,
+ onPressed: notifier.canSubmitTerms
+ ? () async {
+ final success = await notifier.submitTerms();
+ if (success && context.mounted) {
+ context.go(RoutePaths.onboardingBirthDate);
+ }
+ }
+ : null,
+ ),
+ SizedBox(height: 32.h),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ void _showTermsDetail(BuildContext context, String title) {
+ showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ backgroundColor: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
+ ),
+ builder: (context) => DraggableScrollableSheet(
+ initialChildSize: 0.7,
+ minChildSize: 0.5,
+ maxChildSize: 0.9,
+ expand: false,
+ builder: (context, scrollController) => Padding(
+ padding: EdgeInsets.all(24.w),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ title,
+ style: TextStyle(
+ fontSize: 18.sp,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ SizedBox(height: 16.h),
+ Expanded(
+ child: SingleChildScrollView(
+ controller: scrollController,
+ child: Text(
+ _getTermsContent(title),
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: AppColors.gray700,
+ height: 1.6,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ String _getTermsContent(String title) {
+ // TODO: ์ค์ ์ฝ๊ด ๋ด์ฉ์ผ๋ก ๊ต์ฒด
+ return '''
+$title
+
+์ 1์กฐ (๋ชฉ์ )
+์ด ์ฝ๊ด์ MapSy(์ดํ "ํ์ฌ")๊ฐ ์ ๊ณตํ๋ ์๋น์ค์ ์ด์ฉ์กฐ๊ฑด ๋ฐ ์ ์ฐจ, ํ์ฌ์ ํ์ ๊ฐ์ ๊ถ๋ฆฌ, ์๋ฌด ๋ฐ ์ฑ
์์ฌํญ, ๊ธฐํ ํ์ํ ์ฌํญ์ ๊ท์ ํจ์ ๋ชฉ์ ์ผ๋ก ํฉ๋๋ค.
+
+์ 2์กฐ (์ ์)
+1. "์๋น์ค"๋ผ ํจ์ ํ์ฌ๊ฐ ์ ๊ณตํ๋ ๋ชจ๋ ์๋น์ค๋ฅผ ์๋ฏธํฉ๋๋ค.
+2. "ํ์"์ด๋ผ ํจ์ ํ์ฌ์ ์๋น์ค ์ด์ฉ๊ณ์ฝ์ ์ฒด๊ฒฐํ๊ณ ํ์ฌ๊ฐ ์ ๊ณตํ๋ ์๋น์ค๋ฅผ ์ด์ฉํ๋ ์๋ฅผ ๋งํฉ๋๋ค.
+
+์ 3์กฐ (์ฝ๊ด์ ํจ๋ ฅ ๋ฐ ๋ณ๊ฒฝ)
+1. ์ด ์ฝ๊ด์ ์๋น์ค ํ๋ฉด์ ๊ฒ์ํ๊ฑฐ๋ ๊ธฐํ์ ๋ฐฉ๋ฒ์ผ๋ก ํ์์๊ฒ ๊ณต์งํจ์ผ๋ก์จ ํจ๋ ฅ์ด ๋ฐ์ํฉ๋๋ค.
+2. ํ์ฌ๋ ํฉ๋ฆฌ์ ์ธ ์ฌ์ ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ ๊ด๋ จ ๋ฒ๋ น์ ์๋ฐฐ๋์ง ์๋ ๋ฒ์ ๋ด์์ ์ฝ๊ด์ ๋ณ๊ฒฝํ ์ ์์ต๋๋ค.
+
+(์ดํ ์๋ต...)
+''';
+ }
+}
+
+/// ์ ์ฒด ๋์ ์์ดํ
+class _AllAgreeItem extends StatelessWidget {
+ final bool isChecked;
+ final VoidCallback onTap;
+
+ const _AllAgreeItem({
+ required this.isChecked,
+ required this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return InkWell(
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(12.r),
+ child: Container(
+ padding: EdgeInsets.all(16.w),
+ decoration: BoxDecoration(
+ color: isChecked ? AppColors.primary.withValues(alpha: 0.1) : AppColors.gray100,
+ borderRadius: BorderRadius.circular(12.r),
+ border: Border.all(
+ color: isChecked ? AppColors.primary : AppColors.gray200,
+ ),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ isChecked ? Icons.check_circle : Icons.check_circle_outline,
+ color: isChecked ? AppColors.primary : AppColors.gray400,
+ size: 24.w,
+ ),
+ SizedBox(width: 12.w),
+ Text(
+ '์ ์ฒด ๋์ํ๊ธฐ',
+ style: TextStyle(
+ fontSize: 16.sp,
+ fontWeight: FontWeight.w600,
+ color: AppColors.gray900,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+/// ๊ฐ๋ณ ์ฝ๊ด ์์ดํ
+class _AgreementItem extends StatelessWidget {
+ final String title;
+ final bool isChecked;
+ final VoidCallback onTap;
+ final VoidCallback onDetailTap;
+
+ const _AgreementItem({
+ required this.title,
+ required this.isChecked,
+ required this.onTap,
+ required this.onDetailTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ children: [
+ InkWell(
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(4.r),
+ child: Row(
+ children: [
+ Icon(
+ isChecked ? Icons.check_circle : Icons.check_circle_outline,
+ color: isChecked ? AppColors.primary : AppColors.gray400,
+ size: 22.w,
+ ),
+ SizedBox(width: 8.w),
+ Text(
+ title,
+ style: TextStyle(
+ fontSize: 15.sp,
+ color: AppColors.gray700,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const Spacer(),
+ InkWell(
+ onTap: onDetailTap,
+ child: Icon(
+ Icons.chevron_right,
+ color: AppColors.gray400,
+ size: 20.w,
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/features/onboarding/presentation/providers/onboarding_provider.dart b/lib/features/onboarding/presentation/providers/onboarding_provider.dart
new file mode 100644
index 0000000..c8f3ed6
--- /dev/null
+++ b/lib/features/onboarding/presentation/providers/onboarding_provider.dart
@@ -0,0 +1,316 @@
+import 'dart:async';
+import 'package:flutter/foundation.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../../../../core/errors/app_exception.dart';
+import '../../../auth/domain/entities/onboarding_step.dart';
+import '../../data/models/gender_request.dart';
+import '../../data/repositories/onboarding_repository_impl.dart';
+import '../../domain/repositories/onboarding_repository.dart';
+
+// OnboardingStep re-export for convenience
+export '../../../auth/domain/entities/onboarding_step.dart';
+
+part 'onboarding_provider.freezed.dart';
+part 'onboarding_provider.g.dart';
+
+/// ์จ๋ณด๋ฉ ์ํ
+@freezed
+class OnboardingState with _$OnboardingState {
+ const factory OnboardingState({
+ /// ํ์ฌ ๋จ๊ณ
+ @Default(OnboardingStep.terms) OnboardingStep currentStep,
+
+ /// ์ฝ๊ด ๋์ ์ํ
+ @Default(false) bool serviceAgreed,
+ @Default(false) bool privacyAgreed,
+ @Default(false) bool marketingAgreed,
+
+ /// ์๋
์์ผ
+ DateTime? birthDate,
+
+ /// ์ฑ๋ณ
+ Gender? gender,
+
+ /// ๋๋ค์
+ String? nickname,
+
+ /// ๋๋ค์ ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ (null = ํ์ธ ์ํจ)
+ bool? nicknameAvailable,
+
+ /// ๋ก๋ฉ ์ํ
+ @Default(false) bool isLoading,
+
+ /// ์๋ฌ ๋ฉ์์ง
+ String? errorMessage,
+ }) = _OnboardingState;
+}
+
+/// ์จ๋ณด๋ฉ Notifier
+@riverpod
+class OnboardingNotifier extends _$OnboardingNotifier {
+ @override
+ OnboardingState build() {
+ return const OnboardingState();
+ }
+
+ OnboardingRepository get _repository => ref.read(onboardingRepositoryProvider);
+
+ /// ํ์ฌ ๋จ๊ณ ์ค์ (๋ผ์ฐํฐ์์ ํธ์ถ)
+ void setCurrentStep(OnboardingStep step) {
+ state = state.copyWith(currentStep: step);
+ }
+
+ /// ์๋น์ค ์ฝ๊ด ๋์ ํ ๊ธ
+ void toggleServiceAgreement() {
+ state = state.copyWith(serviceAgreed: !state.serviceAgreed);
+ }
+
+ /// ๊ฐ์ธ์ ๋ณด ์ฒ๋ฆฌ๋ฐฉ์นจ ๋์ ํ ๊ธ
+ void togglePrivacyAgreement() {
+ state = state.copyWith(privacyAgreed: !state.privacyAgreed);
+ }
+
+ /// ๋ง์ผํ
์์ ๋์ ํ ๊ธ
+ void toggleMarketingAgreement() {
+ state = state.copyWith(marketingAgreed: !state.marketingAgreed);
+ }
+
+ /// ์ ์ฒด ๋์ (ํ์ ์ฝ๊ด๋ง)
+ void agreeAll() {
+ state = state.copyWith(
+ serviceAgreed: true,
+ privacyAgreed: true,
+ marketingAgreed: true,
+ );
+ }
+
+ /// ์ฝ๊ด ์ ์ถ ๊ฐ๋ฅ ์ฌ๋ถ
+ bool get canSubmitTerms => state.serviceAgreed && state.privacyAgreed;
+
+ /// ์ฝ๊ด ๋์ ์ ์ถ
+ Future submitTerms() async {
+ if (!canSubmitTerms) return false;
+
+ state = state.copyWith(isLoading: true, errorMessage: null);
+
+ try {
+ await _repository.submitTerms(
+ serviceAgreement: state.serviceAgreed,
+ privacyAgreement: state.privacyAgreed,
+ marketingAgreement: state.marketingAgreed,
+ );
+
+ state = state.copyWith(
+ isLoading: false,
+ currentStep: OnboardingStep.birthDate,
+ );
+
+ return true;
+ } catch (e) {
+ state = state.copyWith(
+ isLoading: false,
+ errorMessage: _getErrorMessage(e),
+ );
+ return false;
+ }
+ }
+
+ /// ์๋
์์ผ ์ค์
+ void setBirthDate(DateTime date) {
+ state = state.copyWith(birthDate: date);
+ }
+
+ /// ์๋
์์ผ ์ ์ถ ๊ฐ๋ฅ ์ฌ๋ถ
+ bool get canSubmitBirthDate => state.birthDate != null;
+
+ /// ์๋
์์ผ ์ ์ถ
+ Future submitBirthDate() async {
+ if (!canSubmitBirthDate || state.birthDate == null) return false;
+
+ state = state.copyWith(isLoading: true, errorMessage: null);
+
+ try {
+ await _repository.submitBirthDate(state.birthDate!);
+
+ state = state.copyWith(
+ isLoading: false,
+ currentStep: OnboardingStep.gender,
+ );
+
+ return true;
+ } catch (e) {
+ state = state.copyWith(
+ isLoading: false,
+ errorMessage: _getErrorMessage(e),
+ );
+ return false;
+ }
+ }
+
+ /// ์ฑ๋ณ ์ค์
+ void setGender(Gender gender) {
+ state = state.copyWith(gender: gender);
+ }
+
+ /// ์ฑ๋ณ ์ ์ถ ๊ฐ๋ฅ ์ฌ๋ถ
+ bool get canSubmitGender => state.gender != null;
+
+ /// ์ฑ๋ณ ์ ์ถ
+ Future submitGender() async {
+ if (!canSubmitGender || state.gender == null) return false;
+
+ state = state.copyWith(isLoading: true, errorMessage: null);
+
+ try {
+ await _repository.submitGender(state.gender!);
+
+ state = state.copyWith(
+ isLoading: false,
+ currentStep: OnboardingStep.nickname,
+ );
+
+ return true;
+ } catch (e) {
+ state = state.copyWith(
+ isLoading: false,
+ errorMessage: _getErrorMessage(e),
+ );
+ return false;
+ }
+ }
+
+ // ============================================
+ // ๋๋ค์ ๊ด๋ จ ์์
+ // ============================================
+ static const int _minNicknameLength = 2;
+ static const int _maxNicknameLength = 20;
+
+ /// ๋๋ค์ ์ ํจ์ฑ ๊ฒ์ฆ ์ ๊ท์ (ํ๊ธ, ์๋ฌธ, ์ซ์๋ง ํ์ฉ)
+ static final RegExp _nicknameRegex = RegExp(r'^[๊ฐ-ํฃa-zA-Z0-9]+$');
+
+ /// ๋๋ค์ ์ค์
+ void setNickname(String nickname) {
+ state = state.copyWith(
+ nickname: nickname,
+ nicknameAvailable: null, // ๋๋ค์ ๋ณ๊ฒฝ ์ ํ์ธ ์ํ ์ด๊ธฐํ
+ errorMessage: null,
+ );
+ }
+
+ /// ๋๋ค์ ์ ํจ์ฑ ๊ฒ์ฆ
+ ///
+ /// **๊ท์น**:
+ /// - 2~20์ ์ด๋ด
+ /// - ํ๊ธ, ์๋ฌธ, ์ซ์๋ง ์ฌ์ฉ ๊ฐ๋ฅ
+ /// - ํน์๋ฌธ์ ๋ฐ ๊ณต๋ฐฑ ๋ถ๊ฐ
+ ///
+ /// Returns: ์ ํจํ๋ฉด null, ์ ํจํ์ง ์์ผ๋ฉด ์๋ฌ ๋ฉ์์ง
+ String? validateNickname(String? nickname) {
+ if (nickname == null || nickname.isEmpty) {
+ return '๋๋ค์์ ์
๋ ฅํด์ฃผ์ธ์.';
+ }
+ if (nickname.length < _minNicknameLength) {
+ return '๋๋ค์์ $_minNicknameLength์ ์ด์์ด์ด์ผ ํฉ๋๋ค.';
+ }
+ if (nickname.length > _maxNicknameLength) {
+ return '๋๋ค์์ $_maxNicknameLength์ ์ดํ์ฌ์ผ ํฉ๋๋ค.';
+ }
+ if (nickname.contains(' ')) {
+ return '๋๋ค์์ ๊ณต๋ฐฑ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.';
+ }
+ if (!_nicknameRegex.hasMatch(nickname)) {
+ return '๋๋ค์์ ํ๊ธ, ์๋ฌธ, ์ซ์๋ง ์ฌ์ฉํ ์ ์์ต๋๋ค.';
+ }
+ return null; // ์ ํจํจ
+ }
+
+ /// ๋๋ค์์ด ์ ํจํ์ง ํ์ธ (๋ถ๋ฆฐ ๋ฐํ)
+ bool isNicknameFormatValid(String? nickname) {
+ return validateNickname(nickname) == null;
+ }
+
+ /// ๋๋ค์ ์ค๋ณต ํ์ธ
+ ///
+ /// ๋๋ค์ ํ์์ด ์ ํจํ ๊ฒฝ์ฐ์๋ง ์๋ฒ์ ์ค๋ณต ํ์ธ ์์ฒญ
+ Future checkNickname() async {
+ final nickname = state.nickname;
+
+ // ํ์ ์ ํจ์ฑ ๊ฒ์ฆ ๋จผ์ ์ํ
+ final validationError = validateNickname(nickname);
+ if (validationError != null) {
+ state = state.copyWith(
+ nicknameAvailable: false,
+ errorMessage: validationError,
+ );
+ return false;
+ }
+
+ state = state.copyWith(isLoading: true, errorMessage: null);
+
+ try {
+ final response = await _repository.checkName(nickname!);
+
+ state = state.copyWith(
+ isLoading: false,
+ nicknameAvailable: response.available,
+ errorMessage: response.available ? null : response.message ?? '์ด๋ฏธ ์ฌ์ฉ ์ค์ธ ๋๋ค์์
๋๋ค.',
+ );
+
+ return response.available;
+ } catch (e) {
+ state = state.copyWith(
+ isLoading: false,
+ nicknameAvailable: false,
+ errorMessage: _getErrorMessage(e),
+ );
+ return false;
+ }
+ }
+
+ /// ๋๋ค์ ์ ์ถ ๊ฐ๋ฅ ์ฌ๋ถ
+ bool get canSubmitNickname =>
+ state.nickname != null &&
+ state.nickname!.isNotEmpty &&
+ isNicknameFormatValid(state.nickname) &&
+ state.nicknameAvailable == true;
+
+ /// ํ๋กํ(๋๋ค์) ์ ์ถ
+ Future submitProfile() async {
+ if (!canSubmitNickname || state.nickname == null) return false;
+
+ state = state.copyWith(isLoading: true, errorMessage: null);
+
+ try {
+ await _repository.submitProfile(state.nickname!);
+
+ state = state.copyWith(
+ isLoading: false,
+ currentStep: OnboardingStep.completed,
+ );
+
+ return true;
+ } catch (e) {
+ state = state.copyWith(
+ isLoading: false,
+ errorMessage: _getErrorMessage(e),
+ );
+ return false;
+ }
+ }
+
+ /// ์๋ฌ ๋ฉ์์ง ์ด๊ธฐํ
+ void clearError() {
+ state = state.copyWith(errorMessage: null);
+ }
+
+ /// ์๋ฌ ๋ฉ์์ง ์ถ์ถ
+ String _getErrorMessage(Object error) {
+ if (error is AppException) {
+ return error.message;
+ }
+ debugPrint('โ OnboardingNotifier error: $error');
+ return '์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์.';
+ }
+}
diff --git a/lib/features/onboarding/presentation/providers/onboarding_provider.freezed.dart b/lib/features/onboarding/presentation/providers/onboarding_provider.freezed.dart
new file mode 100644
index 0000000..e7bbef5
--- /dev/null
+++ b/lib/features/onboarding/presentation/providers/onboarding_provider.freezed.dart
@@ -0,0 +1,433 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'onboarding_provider.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
+);
+
+/// @nodoc
+mixin _$OnboardingState {
+ /// ํ์ฌ ๋จ๊ณ
+ OnboardingStep get currentStep => throw _privateConstructorUsedError;
+
+ /// ์ฝ๊ด ๋์ ์ํ
+ bool get serviceAgreed => throw _privateConstructorUsedError;
+ bool get privacyAgreed => throw _privateConstructorUsedError;
+ bool get marketingAgreed => throw _privateConstructorUsedError;
+
+ /// ์๋
์์ผ
+ DateTime? get birthDate => throw _privateConstructorUsedError;
+
+ /// ์ฑ๋ณ
+ Gender? get gender => throw _privateConstructorUsedError;
+
+ /// ๋๋ค์
+ String? get nickname => throw _privateConstructorUsedError;
+
+ /// ๋๋ค์ ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ (null = ํ์ธ ์ํจ)
+ bool? get nicknameAvailable => throw _privateConstructorUsedError;
+
+ /// ๋ก๋ฉ ์ํ
+ bool get isLoading => throw _privateConstructorUsedError;
+
+ /// ์๋ฌ ๋ฉ์์ง
+ String? get errorMessage => throw _privateConstructorUsedError;
+
+ /// Create a copy of OnboardingState
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $OnboardingStateCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $OnboardingStateCopyWith<$Res> {
+ factory $OnboardingStateCopyWith(
+ OnboardingState value,
+ $Res Function(OnboardingState) then,
+ ) = _$OnboardingStateCopyWithImpl<$Res, OnboardingState>;
+ @useResult
+ $Res call({
+ OnboardingStep currentStep,
+ bool serviceAgreed,
+ bool privacyAgreed,
+ bool marketingAgreed,
+ DateTime? birthDate,
+ Gender? gender,
+ String? nickname,
+ bool? nicknameAvailable,
+ bool isLoading,
+ String? errorMessage,
+ });
+}
+
+/// @nodoc
+class _$OnboardingStateCopyWithImpl<$Res, $Val extends OnboardingState>
+ implements $OnboardingStateCopyWith<$Res> {
+ _$OnboardingStateCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of OnboardingState
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? currentStep = null,
+ Object? serviceAgreed = null,
+ Object? privacyAgreed = null,
+ Object? marketingAgreed = null,
+ Object? birthDate = freezed,
+ Object? gender = freezed,
+ Object? nickname = freezed,
+ Object? nicknameAvailable = freezed,
+ Object? isLoading = null,
+ Object? errorMessage = freezed,
+ }) {
+ return _then(
+ _value.copyWith(
+ currentStep: null == currentStep
+ ? _value.currentStep
+ : currentStep // ignore: cast_nullable_to_non_nullable
+ as OnboardingStep,
+ serviceAgreed: null == serviceAgreed
+ ? _value.serviceAgreed
+ : serviceAgreed // ignore: cast_nullable_to_non_nullable
+ as bool,
+ privacyAgreed: null == privacyAgreed
+ ? _value.privacyAgreed
+ : privacyAgreed // ignore: cast_nullable_to_non_nullable
+ as bool,
+ marketingAgreed: null == marketingAgreed
+ ? _value.marketingAgreed
+ : marketingAgreed // ignore: cast_nullable_to_non_nullable
+ as bool,
+ birthDate: freezed == birthDate
+ ? _value.birthDate
+ : birthDate // ignore: cast_nullable_to_non_nullable
+ as DateTime?,
+ gender: freezed == gender
+ ? _value.gender
+ : gender // ignore: cast_nullable_to_non_nullable
+ as Gender?,
+ nickname: freezed == nickname
+ ? _value.nickname
+ : nickname // ignore: cast_nullable_to_non_nullable
+ as String?,
+ nicknameAvailable: freezed == nicknameAvailable
+ ? _value.nicknameAvailable
+ : nicknameAvailable // ignore: cast_nullable_to_non_nullable
+ as bool?,
+ isLoading: null == isLoading
+ ? _value.isLoading
+ : isLoading // ignore: cast_nullable_to_non_nullable
+ as bool,
+ errorMessage: freezed == errorMessage
+ ? _value.errorMessage
+ : errorMessage // ignore: cast_nullable_to_non_nullable
+ as String?,
+ )
+ as $Val,
+ );
+ }
+}
+
+/// @nodoc
+abstract class _$$OnboardingStateImplCopyWith<$Res>
+ implements $OnboardingStateCopyWith<$Res> {
+ factory _$$OnboardingStateImplCopyWith(
+ _$OnboardingStateImpl value,
+ $Res Function(_$OnboardingStateImpl) then,
+ ) = __$$OnboardingStateImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({
+ OnboardingStep currentStep,
+ bool serviceAgreed,
+ bool privacyAgreed,
+ bool marketingAgreed,
+ DateTime? birthDate,
+ Gender? gender,
+ String? nickname,
+ bool? nicknameAvailable,
+ bool isLoading,
+ String? errorMessage,
+ });
+}
+
+/// @nodoc
+class __$$OnboardingStateImplCopyWithImpl<$Res>
+ extends _$OnboardingStateCopyWithImpl<$Res, _$OnboardingStateImpl>
+ implements _$$OnboardingStateImplCopyWith<$Res> {
+ __$$OnboardingStateImplCopyWithImpl(
+ _$OnboardingStateImpl _value,
+ $Res Function(_$OnboardingStateImpl) _then,
+ ) : super(_value, _then);
+
+ /// Create a copy of OnboardingState
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? currentStep = null,
+ Object? serviceAgreed = null,
+ Object? privacyAgreed = null,
+ Object? marketingAgreed = null,
+ Object? birthDate = freezed,
+ Object? gender = freezed,
+ Object? nickname = freezed,
+ Object? nicknameAvailable = freezed,
+ Object? isLoading = null,
+ Object? errorMessage = freezed,
+ }) {
+ return _then(
+ _$OnboardingStateImpl(
+ currentStep: null == currentStep
+ ? _value.currentStep
+ : currentStep // ignore: cast_nullable_to_non_nullable
+ as OnboardingStep,
+ serviceAgreed: null == serviceAgreed
+ ? _value.serviceAgreed
+ : serviceAgreed // ignore: cast_nullable_to_non_nullable
+ as bool,
+ privacyAgreed: null == privacyAgreed
+ ? _value.privacyAgreed
+ : privacyAgreed // ignore: cast_nullable_to_non_nullable
+ as bool,
+ marketingAgreed: null == marketingAgreed
+ ? _value.marketingAgreed
+ : marketingAgreed // ignore: cast_nullable_to_non_nullable
+ as bool,
+ birthDate: freezed == birthDate
+ ? _value.birthDate
+ : birthDate // ignore: cast_nullable_to_non_nullable
+ as DateTime?,
+ gender: freezed == gender
+ ? _value.gender
+ : gender // ignore: cast_nullable_to_non_nullable
+ as Gender?,
+ nickname: freezed == nickname
+ ? _value.nickname
+ : nickname // ignore: cast_nullable_to_non_nullable
+ as String?,
+ nicknameAvailable: freezed == nicknameAvailable
+ ? _value.nicknameAvailable
+ : nicknameAvailable // ignore: cast_nullable_to_non_nullable
+ as bool?,
+ isLoading: null == isLoading
+ ? _value.isLoading
+ : isLoading // ignore: cast_nullable_to_non_nullable
+ as bool,
+ errorMessage: freezed == errorMessage
+ ? _value.errorMessage
+ : errorMessage // ignore: cast_nullable_to_non_nullable
+ as String?,
+ ),
+ );
+ }
+}
+
+/// @nodoc
+
+class _$OnboardingStateImpl
+ with DiagnosticableTreeMixin
+ implements _OnboardingState {
+ const _$OnboardingStateImpl({
+ this.currentStep = OnboardingStep.terms,
+ this.serviceAgreed = false,
+ this.privacyAgreed = false,
+ this.marketingAgreed = false,
+ this.birthDate,
+ this.gender,
+ this.nickname,
+ this.nicknameAvailable,
+ this.isLoading = false,
+ this.errorMessage,
+ });
+
+ /// ํ์ฌ ๋จ๊ณ
+ @override
+ @JsonKey()
+ final OnboardingStep currentStep;
+
+ /// ์ฝ๊ด ๋์ ์ํ
+ @override
+ @JsonKey()
+ final bool serviceAgreed;
+ @override
+ @JsonKey()
+ final bool privacyAgreed;
+ @override
+ @JsonKey()
+ final bool marketingAgreed;
+
+ /// ์๋
์์ผ
+ @override
+ final DateTime? birthDate;
+
+ /// ์ฑ๋ณ
+ @override
+ final Gender? gender;
+
+ /// ๋๋ค์
+ @override
+ final String? nickname;
+
+ /// ๋๋ค์ ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ (null = ํ์ธ ์ํจ)
+ @override
+ final bool? nicknameAvailable;
+
+ /// ๋ก๋ฉ ์ํ
+ @override
+ @JsonKey()
+ final bool isLoading;
+
+ /// ์๋ฌ ๋ฉ์์ง
+ @override
+ final String? errorMessage;
+
+ @override
+ String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
+ return 'OnboardingState(currentStep: $currentStep, serviceAgreed: $serviceAgreed, privacyAgreed: $privacyAgreed, marketingAgreed: $marketingAgreed, birthDate: $birthDate, gender: $gender, nickname: $nickname, nicknameAvailable: $nicknameAvailable, isLoading: $isLoading, errorMessage: $errorMessage)';
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties
+ ..add(DiagnosticsProperty('type', 'OnboardingState'))
+ ..add(DiagnosticsProperty('currentStep', currentStep))
+ ..add(DiagnosticsProperty('serviceAgreed', serviceAgreed))
+ ..add(DiagnosticsProperty('privacyAgreed', privacyAgreed))
+ ..add(DiagnosticsProperty('marketingAgreed', marketingAgreed))
+ ..add(DiagnosticsProperty('birthDate', birthDate))
+ ..add(DiagnosticsProperty('gender', gender))
+ ..add(DiagnosticsProperty('nickname', nickname))
+ ..add(DiagnosticsProperty('nicknameAvailable', nicknameAvailable))
+ ..add(DiagnosticsProperty('isLoading', isLoading))
+ ..add(DiagnosticsProperty('errorMessage', errorMessage));
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$OnboardingStateImpl &&
+ (identical(other.currentStep, currentStep) ||
+ other.currentStep == currentStep) &&
+ (identical(other.serviceAgreed, serviceAgreed) ||
+ other.serviceAgreed == serviceAgreed) &&
+ (identical(other.privacyAgreed, privacyAgreed) ||
+ other.privacyAgreed == privacyAgreed) &&
+ (identical(other.marketingAgreed, marketingAgreed) ||
+ other.marketingAgreed == marketingAgreed) &&
+ (identical(other.birthDate, birthDate) ||
+ other.birthDate == birthDate) &&
+ (identical(other.gender, gender) || other.gender == gender) &&
+ (identical(other.nickname, nickname) ||
+ other.nickname == nickname) &&
+ (identical(other.nicknameAvailable, nicknameAvailable) ||
+ other.nicknameAvailable == nicknameAvailable) &&
+ (identical(other.isLoading, isLoading) ||
+ other.isLoading == isLoading) &&
+ (identical(other.errorMessage, errorMessage) ||
+ other.errorMessage == errorMessage));
+ }
+
+ @override
+ int get hashCode => Object.hash(
+ runtimeType,
+ currentStep,
+ serviceAgreed,
+ privacyAgreed,
+ marketingAgreed,
+ birthDate,
+ gender,
+ nickname,
+ nicknameAvailable,
+ isLoading,
+ errorMessage,
+ );
+
+ /// Create a copy of OnboardingState
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$OnboardingStateImplCopyWith<_$OnboardingStateImpl> get copyWith =>
+ __$$OnboardingStateImplCopyWithImpl<_$OnboardingStateImpl>(
+ this,
+ _$identity,
+ );
+}
+
+abstract class _OnboardingState implements OnboardingState {
+ const factory _OnboardingState({
+ final OnboardingStep currentStep,
+ final bool serviceAgreed,
+ final bool privacyAgreed,
+ final bool marketingAgreed,
+ final DateTime? birthDate,
+ final Gender? gender,
+ final String? nickname,
+ final bool? nicknameAvailable,
+ final bool isLoading,
+ final String? errorMessage,
+ }) = _$OnboardingStateImpl;
+
+ /// ํ์ฌ ๋จ๊ณ
+ @override
+ OnboardingStep get currentStep;
+
+ /// ์ฝ๊ด ๋์ ์ํ
+ @override
+ bool get serviceAgreed;
+ @override
+ bool get privacyAgreed;
+ @override
+ bool get marketingAgreed;
+
+ /// ์๋
์์ผ
+ @override
+ DateTime? get birthDate;
+
+ /// ์ฑ๋ณ
+ @override
+ Gender? get gender;
+
+ /// ๋๋ค์
+ @override
+ String? get nickname;
+
+ /// ๋๋ค์ ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ (null = ํ์ธ ์ํจ)
+ @override
+ bool? get nicknameAvailable;
+
+ /// ๋ก๋ฉ ์ํ
+ @override
+ bool get isLoading;
+
+ /// ์๋ฌ ๋ฉ์์ง
+ @override
+ String? get errorMessage;
+
+ /// Create a copy of OnboardingState
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$OnboardingStateImplCopyWith<_$OnboardingStateImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/features/onboarding/presentation/providers/onboarding_provider.g.dart b/lib/features/onboarding/presentation/providers/onboarding_provider.g.dart
new file mode 100644
index 0000000..dad3598
--- /dev/null
+++ b/lib/features/onboarding/presentation/providers/onboarding_provider.g.dart
@@ -0,0 +1,29 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'onboarding_provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$onboardingNotifierHash() =>
+ r'4782c882b4809345d6b62241c9809feccdc9beab';
+
+/// ์จ๋ณด๋ฉ Notifier
+///
+/// Copied from [OnboardingNotifier].
+@ProviderFor(OnboardingNotifier)
+final onboardingNotifierProvider =
+ AutoDisposeNotifierProvider.internal(
+ OnboardingNotifier.new,
+ name: r'onboardingNotifierProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$onboardingNotifierHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+ );
+
+typedef _$OnboardingNotifier = AutoDisposeNotifier;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
diff --git a/lib/features/onboarding/presentation/widgets/onboarding_button.dart b/lib/features/onboarding/presentation/widgets/onboarding_button.dart
new file mode 100644
index 0000000..19bf4e9
--- /dev/null
+++ b/lib/features/onboarding/presentation/widgets/onboarding_button.dart
@@ -0,0 +1,84 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+import '../../../../core/constants/app_colors.dart';
+
+/// ์จ๋ณด๋ฉ ๊ณตํต ๋ฒํผ
+class OnboardingButton extends StatelessWidget {
+ final String text;
+ final VoidCallback? onPressed;
+ final bool isLoading;
+
+ const OnboardingButton({
+ super.key,
+ required this.text,
+ this.onPressed,
+ this.isLoading = false,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final isEnabled = onPressed != null && !isLoading;
+
+ return SizedBox(
+ width: double.infinity,
+ height: 56.h,
+ child: ElevatedButton(
+ onPressed: isEnabled ? onPressed : null,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: isEnabled ? AppColors.primary : AppColors.gray300,
+ foregroundColor: Colors.white,
+ disabledBackgroundColor: AppColors.gray300,
+ disabledForegroundColor: AppColors.gray500,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12.r),
+ ),
+ elevation: 0,
+ ),
+ child: isLoading
+ ? SizedBox(
+ width: 24.w,
+ height: 24.h,
+ child: const CircularProgressIndicator(
+ color: Colors.white,
+ strokeWidth: 2,
+ ),
+ )
+ : Text(
+ text,
+ style: TextStyle(
+ fontSize: 16.sp,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+/// ์จ๋ณด๋ฉ ํ
์คํธ ๋ฒํผ (๊ฑด๋๋ฐ๊ธฐ ๋ฑ)
+class OnboardingTextButton extends StatelessWidget {
+ final String text;
+ final VoidCallback? onPressed;
+
+ const OnboardingTextButton({
+ super.key,
+ required this.text,
+ this.onPressed,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return TextButton(
+ onPressed: onPressed,
+ child: Text(
+ text,
+ style: TextStyle(
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w500,
+ color: AppColors.gray600,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/onboarding/presentation/widgets/step_indicator.dart b/lib/features/onboarding/presentation/widgets/step_indicator.dart
new file mode 100644
index 0000000..b6782df
--- /dev/null
+++ b/lib/features/onboarding/presentation/widgets/step_indicator.dart
@@ -0,0 +1,109 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+import '../../../../core/constants/app_colors.dart';
+import '../providers/onboarding_provider.dart';
+
+/// ์จ๋ณด๋ฉ ์งํ ํ์๊ธฐ
+class StepIndicator extends StatelessWidget {
+ final OnboardingStep currentStep;
+ final int totalSteps;
+
+ const StepIndicator({
+ super.key,
+ required this.currentStep,
+ this.totalSteps = 4,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: List.generate(
+ totalSteps,
+ (index) => _buildDot(index),
+ ),
+ );
+ }
+
+ Widget _buildDot(int index) {
+ final isActive = index == currentStep.index;
+ final isCompleted = index < currentStep.index;
+
+ return Container(
+ margin: EdgeInsets.symmetric(horizontal: 4.w),
+ width: isActive ? 24.w : 8.w,
+ height: 8.h,
+ decoration: BoxDecoration(
+ color: isActive || isCompleted ? AppColors.primary : AppColors.gray300,
+ borderRadius: BorderRadius.circular(4.r),
+ ),
+ );
+ }
+}
+
+/// ์จ๋ณด๋ฉ ์งํ๋ฅ ๋ฐ
+class StepProgressBar extends StatelessWidget {
+ final OnboardingStep currentStep;
+ final int totalSteps;
+
+ const StepProgressBar({
+ super.key,
+ required this.currentStep,
+ this.totalSteps = 4,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final progress = (currentStep.index + 1) / totalSteps;
+
+ return Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ _getStepText(),
+ style: TextStyle(
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w500,
+ color: AppColors.gray600,
+ ),
+ ),
+ Text(
+ '${currentStep.index + 1}/$totalSteps',
+ style: TextStyle(
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w500,
+ color: AppColors.primary,
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 8.h),
+ LinearProgressIndicator(
+ value: progress,
+ backgroundColor: AppColors.gray200,
+ valueColor: const AlwaysStoppedAnimation(AppColors.primary),
+ minHeight: 4.h,
+ borderRadius: BorderRadius.circular(2.r),
+ ),
+ ],
+ );
+ }
+
+ String _getStepText() {
+ switch (currentStep) {
+ case OnboardingStep.terms:
+ return '์ฝ๊ด ๋์';
+ case OnboardingStep.birthDate:
+ return '์๋
์์ผ';
+ case OnboardingStep.gender:
+ return '์ฑ๋ณ';
+ case OnboardingStep.nickname:
+ return '๋๋ค์';
+ case OnboardingStep.completed:
+ return '์๋ฃ';
+ }
+ }
+}
diff --git a/lib/router/app_router.dart b/lib/router/app_router.dart
index 96e9f7b..36e3651 100644
--- a/lib/router/app_router.dart
+++ b/lib/router/app_router.dart
@@ -15,6 +15,12 @@ import '../features/auth/presentation/pages/login_page.dart';
import '../features/auth/presentation/pages/onboarding_page.dart';
import '../features/home/presentation/pages/home_page.dart';
+// Onboarding Step Pages
+import '../features/onboarding/presentation/pages/terms_step_page.dart';
+import '../features/onboarding/presentation/pages/birth_date_step_page.dart';
+import '../features/onboarding/presentation/pages/gender_step_page.dart';
+import '../features/onboarding/presentation/pages/nickname_step_page.dart';
+
/// GoRouter ์ธ์คํด์ค๋ฅผ ์ ๊ณตํ๋ Riverpod Provider
///
/// ์ฑ ์ ์ฒด์ ๋ค๋น๊ฒ์ด์
์ ๊ด๋ฆฌํ๋ฉฐ, ์ธ์ฆ ์ํ์ ๋ฐ๋ผ
@@ -69,6 +75,15 @@ final routerProvider = Provider((ref) {
// ์ธ์ฆ์ด ๋ถํ์ํ ๊ณต๊ฐ ๊ฒฝ๋ก (Splash, Login)
final publicPaths = [RoutePaths.splash, RoutePaths.login];
+ // ์จ๋ณด๋ฉ ๊ฒฝ๋ก๋ค
+ final onboardingPaths = [
+ RoutePaths.onboarding,
+ RoutePaths.onboardingTerms,
+ RoutePaths.onboardingBirthDate,
+ RoutePaths.onboardingGender,
+ RoutePaths.onboardingNickname,
+ ];
+
// ====================================================================
// 1. ์ธ์ฆ ์ฒดํฌ - ๋ก๊ทธ์ธ ํ์ํ ํ์ด์ง ๋ณดํธ
// ====================================================================
@@ -82,21 +97,21 @@ final routerProvider = Provider((ref) {
}
// ====================================================================
- // 2. ์ธ์ฆ๋ ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ํ์ด์ง ์ ๊ทผ ์
+ // 2. ์ธ์ฆ๋ ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ํ์ด์ง ์ ๊ทผ ์ โ ํ ๋๋ ์จ๋ณด๋ฉ์ผ๋ก
// ====================================================================
if (currentPath == RoutePaths.login) {
- // ๋ก๊ทธ์ธ ์๋ฃ ์ ํ์ผ๋ก
+ // ์จ๋ณด๋ฉ ํ์ ์ฌ๋ถ ํ์ธ (๋๊ธฐ์ ์ผ๋ก ํ์ธ ๋ถ๊ฐํ๋ฏ๋ก ์ผ๋จ ํ์ผ๋ก)
+ // ์ค์ ์จ๋ณด๋ฉ ์ฒดํฌ๋ ๋ก๊ทธ์ธ ํ SignInResponse์์ ์ฒ๋ฆฌ
return RoutePaths.home;
}
// ====================================================================
- // 3. ์จ๋ณด๋ฉ ์ฒดํฌ (ํฅํ ๊ตฌํ)
+ // 3. ์จ๋ณด๋ฉ ์ค์ธ ์ฌ์ฉ์๊ฐ ํ์ ์ ๊ทผ ์ โ ์จ๋ณด๋ฉ์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ
// ====================================================================
- // TODO: ์จ๋ณด๋ฉ ์๋ฃ ์ฌ๋ถ ํ์ธ
- // SharedPreferences๋ Firestore์์ ์จ๋ณด๋ฉ ์๋ฃ ์ํ ํ์ธ
- // if (!onboardingCompleted && currentPath != RoutePaths.onboarding) {
- // return RoutePaths.onboarding;
- // }
+ // ์จ๋ณด๋ฉ ๊ฒฝ๋ก์ ์์ผ๋ฉด ํ์ฉ
+ if (onboardingPaths.contains(currentPath)) {
+ return null;
+ }
return null; // ๋ฆฌ๋ค์ด๋ ํธ ๋ถํ์
},
@@ -123,6 +138,33 @@ final routerProvider = Provider((ref) {
builder: (context, state) => const OnboardingPage(),
),
+ // ====================================================================
+ // Onboarding Step Routes
+ // ====================================================================
+ GoRoute(
+ path: RoutePaths.onboardingTerms,
+ name: RoutePaths.onboardingTermsName,
+ builder: (context, state) => const TermsStepPage(),
+ ),
+
+ GoRoute(
+ path: RoutePaths.onboardingBirthDate,
+ name: RoutePaths.onboardingBirthDateName,
+ builder: (context, state) => const BirthDateStepPage(),
+ ),
+
+ GoRoute(
+ path: RoutePaths.onboardingGender,
+ name: RoutePaths.onboardingGenderName,
+ builder: (context, state) => const GenderStepPage(),
+ ),
+
+ GoRoute(
+ path: RoutePaths.onboardingNickname,
+ name: RoutePaths.onboardingNicknameName,
+ builder: (context, state) => const NicknameStepPage(),
+ ),
+
// ====================================================================
// Home & Main Navigation
// ====================================================================
diff --git a/lib/router/route_paths.dart b/lib/router/route_paths.dart
index 14bc0ce..ef87a72 100644
--- a/lib/router/route_paths.dart
+++ b/lib/router/route_paths.dart
@@ -41,6 +41,18 @@ class RoutePaths {
/// ์จ๋ณด๋ฉ ํ๋ฉด (์ฒซ ๋ก๊ทธ์ธ ์: ์ด์ฉ์ฝ๊ด ๋์, ๋๋ค์ ์ค์ ๋ฑ)
static const String onboarding = '/onboarding';
+ /// ์จ๋ณด๋ฉ - ์ฝ๊ด ๋์
+ static const String onboardingTerms = '/onboarding/terms';
+
+ /// ์จ๋ณด๋ฉ - ์๋
์์ผ
+ static const String onboardingBirthDate = '/onboarding/birth-date';
+
+ /// ์จ๋ณด๋ฉ - ์ฑ๋ณ
+ static const String onboardingGender = '/onboarding/gender';
+
+ /// ์จ๋ณด๋ฉ - ๋๋ค์
+ static const String onboardingNickname = '/onboarding/nickname';
+
/// ํ ํ๋ฉด (์ธ์ฆ ํ์, ๋ฉ์ธ ๊ธฐ๋ฅ ์ง์
์ )
static const String home = '/home';
@@ -52,5 +64,9 @@ class RoutePaths {
static const String splashName = 'splash';
static const String loginName = 'login';
static const String onboardingName = 'onboarding';
+ static const String onboardingTermsName = 'onboarding-terms';
+ static const String onboardingBirthDateName = 'onboarding-birth-date';
+ static const String onboardingGenderName = 'onboarding-gender';
+ static const String onboardingNicknameName = 'onboarding-nickname';
static const String homeName = 'home';
}
diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj
index 926e8ef..a2c4087 100644
--- a/macos/Runner.xcodeproj/project.pbxproj
+++ b/macos/Runner.xcodeproj/project.pbxproj
@@ -21,6 +21,7 @@
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
+ 12BD5999885CD2782FFFAD69 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4137A3D35378E7ED9A986E6 /* Pods_RunnerTests.framework */; };
1B4D4B9641B35979114E19F9 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2905CC2DC438CDE4F7700928 /* GoogleService-Info.plist */; };
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
@@ -28,6 +29,7 @@
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+ FCB8C50EADD81F00E8642AF8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 092444A6C4387385F2B73CD9 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -61,7 +63,11 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 092444A6C4387385F2B73CD9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 1C392A8B03AFD7E60BD0949A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ 20A4A0C95DBEAAB57FA65D07 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
2905CC2DC438CDE4F7700928 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; };
+ 300488F43CF7B11F802CEC95 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; };
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
@@ -78,8 +84,12 @@
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; };
+ 3A1C108895255F12D7CD5DA7 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 5726A8F0DB2D85C96A30EA41 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
+ A4137A3D35378E7ED9A986E6 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ C157EEB9BE94B633651908B7 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -87,6 +97,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 12BD5999885CD2782FFFAD69 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -94,6 +105,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ FCB8C50EADD81F00E8642AF8 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -128,6 +140,7 @@
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
2905CC2DC438CDE4F7700928 /* GoogleService-Info.plist */,
+ 96921C4AE5670A5E25CA234E /* Pods */,
);
sourceTree = "";
};
@@ -175,9 +188,25 @@
path = Runner;
sourceTree = "";
};
+ 96921C4AE5670A5E25CA234E /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 3A1C108895255F12D7CD5DA7 /* Pods-Runner.debug.xcconfig */,
+ 5726A8F0DB2D85C96A30EA41 /* Pods-Runner.release.xcconfig */,
+ 1C392A8B03AFD7E60BD0949A /* Pods-Runner.profile.xcconfig */,
+ 300488F43CF7B11F802CEC95 /* Pods-RunnerTests.debug.xcconfig */,
+ 20A4A0C95DBEAAB57FA65D07 /* Pods-RunnerTests.release.xcconfig */,
+ C157EEB9BE94B633651908B7 /* Pods-RunnerTests.profile.xcconfig */,
+ );
+ name = Pods;
+ path = Pods;
+ sourceTree = "";
+ };
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
+ 092444A6C4387385F2B73CD9 /* Pods_Runner.framework */,
+ A4137A3D35378E7ED9A986E6 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "";
@@ -189,6 +218,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
+ 9A4B57CF7FC060BA107CC516 /* [CP] Check Pods Manifest.lock */,
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
@@ -207,12 +237,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
+ 704003F8267A84DA97BF5147 /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
45631ED06968130C2A63C1B7 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */,
+ 0365A5E78D57A78ED64FEA43 /* [CP] Embed Pods Frameworks */,
+ 3F5F0DD694506B7ECBF23E3B /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -296,6 +329,23 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
+ 0365A5E78D57A78ED64FEA43 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -334,6 +384,23 @@
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
+ 3F5F0DD694506B7ECBF23E3B /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
45631ED06968130C2A63C1B7 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -352,6 +419,50 @@
shellPath = /bin/sh;
shellScript = "\n#!/bin/bash\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:${PUB_CACHE}/bin:$HOME/.pub-cache/bin\"\n\nif [ -z \"$PODS_ROOT\" ] || [ ! -d \"$PODS_ROOT/FirebaseCrashlytics\" ]; then\n # Cannot use \"BUILD_DIR%/Build/*\" as per Firebase documentation, it points to \"flutter-project/build/ios/*\" path which doesn't have run script\n DERIVED_DATA_PATH=$(echo \"$BUILD_ROOT\" | sed -E 's|(.*DerivedData/[^/]+).*|\\1|')\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"${DERIVED_DATA_PATH}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\nelse\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"$PODS_ROOT/FirebaseCrashlytics/run\"\nfi\n\n# Command to upload symbols script used to upload symbols to Firebase server\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT\" --platform=macos --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n";
};
+ 704003F8267A84DA97BF5147 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 9A4B57CF7FC060BA107CC516 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -403,6 +514,7 @@
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 300488F43CF7B11F802CEC95 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
@@ -417,6 +529,7 @@
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 20A4A0C95DBEAAB57FA65D07 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
@@ -431,6 +544,7 @@
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = C157EEB9BE94B633651908B7 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata
index 1d526a1..21a3cc1 100644
--- a/macos/Runner.xcworkspace/contents.xcworkspacedata
+++ b/macos/Runner.xcworkspace/contents.xcworkspacedata
@@ -4,4 +4,7 @@
+
+
diff --git a/pubspec.lock b/pubspec.lock
index 2fa6380..5f6657f 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
- sha256: e4a1b612fd2955908e26116075b3a4baf10c353418ca645b4deae231c82bf144
+ sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182
url: "https://pub.dev"
source: hosted
- version: "1.3.65"
+ version: "1.3.66"
analyzer:
dependency: transitive
description:
@@ -169,6 +169,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
+ code_assets:
+ dependency: transitive
+ description:
+ name: code_assets
+ sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
code_builder:
dependency: transitive
description:
@@ -261,10 +269,10 @@ packages:
dependency: "direct main"
description:
name: dio
- sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
+ sha256: b9d46faecab38fc8cc286f80bc4d61a3bb5d4ac49e51ed877b4d6706efe57b25
url: "https://pub.dev"
source: hosted
- version: "5.9.0"
+ version: "5.9.1"
dio_web_adapter:
dependency: transitive
description:
@@ -301,34 +309,34 @@ packages:
dependency: "direct main"
description:
name: firebase_auth
- sha256: "060a9bfc9877538dfdc69f2fdc1e0bf068439a7e9f22da6513f7b9bde308bb93"
+ sha256: b20d1540460814c5984474c1e9dd833bdbcff6ecd8d6ad86cc9da8cfd581c172
url: "https://pub.dev"
source: hosted
- version: "6.1.3"
+ version: "6.1.4"
firebase_auth_platform_interface:
dependency: transitive
description:
name: firebase_auth_platform_interface
- sha256: e91c9aadf9944e8319855fa752fd6c664bade2bbd6e7697a5142113c319a4288
+ sha256: fd0225320b6bbc92460c86352d16b60aea15f9ef88292774cca97b0522ea9f72
url: "https://pub.dev"
source: hosted
- version: "8.1.5"
+ version: "8.1.6"
firebase_auth_web:
dependency: transitive
description:
name: firebase_auth_web
- sha256: "155145fd84f311e50eb2bfeb892f74b0d1b30936f40765a051b70110270cb6d1"
+ sha256: be7dccb263b89fbda2a564de9d8193118196e8481ffb937222a025cdfdf82c40
url: "https://pub.dev"
source: hosted
- version: "6.1.1"
+ version: "6.1.2"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
- sha256: "29cfa93c771d8105484acac340b5ea0835be371672c91405a300303986f4eba9"
+ sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80"
url: "https://pub.dev"
source: hosted
- version: "4.3.0"
+ version: "4.4.0"
firebase_core_platform_interface:
dependency: transitive
description:
@@ -341,50 +349,50 @@ packages:
dependency: transitive
description:
name: firebase_core_web
- sha256: a631bbfbfa26963d68046aed949df80b228964020e9155b086eff94f462bbf1f
+ sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084"
url: "https://pub.dev"
source: hosted
- version: "3.3.1"
+ version: "3.4.0"
firebase_crashlytics:
dependency: "direct main"
description:
name: firebase_crashlytics
- sha256: "8d52022ee6fdd224e92c042f297d1fd0ec277195c49f39fa61b8cc500a639f00"
+ sha256: a6e6cb8b2ea1214533a54e4c1b11b19c40f6a29333f3ab0854a479fdc3237c5b
url: "https://pub.dev"
source: hosted
- version: "5.0.6"
+ version: "5.0.7"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
- sha256: "97c6a97b35e3d3dafe38fb053a65086a1efb125022d292161405848527cc25a4"
+ sha256: fc6837c4c64c48fa94cab8a872a632b9194fa9208ca76a822f424b3da945584d
url: "https://pub.dev"
source: hosted
- version: "3.8.16"
+ version: "3.8.17"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
- sha256: "1ad663fbb6758acec09d7e84a2e6478265f0a517f40ef77c573efd5e0089f400"
+ sha256: "06fad40ea14771e969a8f2bbce1944aa20ee2f4f57f4eca5b3ba346b65f3f644"
url: "https://pub.dev"
source: hosted
- version: "16.1.0"
+ version: "16.1.1"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
- sha256: ea620e841fbcec62a96984295fc628f53ef5a8da4f53238159719ed0af7db834
+ sha256: "6c49e901c77e6e10e86d98e32056a087eb1ca1b93acdf58524f1961e617657b7"
url: "https://pub.dev"
source: hosted
- version: "4.7.5"
+ version: "4.7.6"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
- sha256: "7d0fb6256202515bba8489a3d69c6bc9d52d69a4999bad789053b486c8e7323e"
+ sha256: "2756f8fea583ffb9d294d15ddecb3a9ad429b023b70c9990c151fc92c54a32b3"
url: "https://pub.dev"
source: hosted
- version: "4.1.1"
+ version: "4.1.2"
fixnum:
dependency: transitive
description:
@@ -632,6 +640,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
+ hooks:
+ dependency: transitive
+ description:
+ name: hooks
+ sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
http:
dependency: transitive
description:
@@ -756,10 +772,10 @@ packages:
dependency: transitive
description:
name: meta
- sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
+ sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
- version: "1.16.0"
+ version: "1.17.0"
mime:
dependency: transitive
description:
@@ -768,6 +784,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
+ native_toolchain_c:
+ dependency: transitive
+ description:
+ name: native_toolchain_c
+ sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.17.4"
+ objective_c:
+ dependency: transitive
+ description:
+ name: objective_c
+ sha256: "7fd0c4d8ac8980011753b9bdaed2bf15111365924cdeeeaeb596214ea2b03537"
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.2.4"
package_config:
dependency: transitive
description:
@@ -828,10 +860,10 @@ packages:
dependency: transitive
description:
name: path_provider_foundation
- sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4"
+ sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
- version: "2.5.1"
+ version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
@@ -896,14 +928,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.3"
- protobuf:
- dependency: transitive
- description:
- name: protobuf
- sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e
- url: "https://pub.dev"
- source: hosted
- version: "4.2.0"
pub_semver:
dependency: transitive
description:
@@ -928,14 +952,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.9.2"
- retrofit_generator:
- dependency: "direct dev"
- description:
- name: retrofit_generator
- sha256: "9abcf21acb95bf7040546eafff87f60cf0aee20b05101d71f99876fc4df1f522"
- url: "https://pub.dev"
- source: hosted
- version: "9.7.0"
riverpod:
dependency: transitive
description:
@@ -980,10 +996,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
- sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
+ sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f
url: "https://pub.dev"
source: hosted
- version: "2.4.18"
+ version: "2.4.20"
shared_preferences_foundation:
dependency: transitive
description:
@@ -1169,10 +1185,10 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
+ sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
- version: "0.7.6"
+ version: "0.7.7"
timezone:
dependency: transitive
description:
@@ -1233,10 +1249,10 @@ packages:
dependency: transitive
description:
name: vector_graphics_compiler
- sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc
+ sha256: "201e876b5d52753626af64b6359cd13ac6011b80728731428fd34bc840f71c9b"
url: "https://pub.dev"
source: hosted
- version: "1.1.19"
+ version: "1.1.20"
vector_math:
dependency: transitive
description:
@@ -1326,5 +1342,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
- dart: ">=3.9.2 <4.0.0"
- flutter: ">=3.35.0"
+ dart: ">=3.10.3 <4.0.0"
+ flutter: ">=3.38.4"