diff --git a/android/app/src/main/res/drawable-v21/background.png b/android/app/src/main/res/drawable-v21/background.png new file mode 100644 index 0000000..8e21404 Binary files /dev/null and b/android/app/src/main/res/drawable-v21/background.png differ diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml index f74085f..f88598c 100644 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -1,12 +1,6 @@ - - - - - + + + diff --git a/android/app/src/main/res/drawable/background.png b/android/app/src/main/res/drawable/background.png new file mode 100644 index 0000000..8e21404 Binary files /dev/null and b/android/app/src/main/res/drawable/background.png differ diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml index 304732f..f88598c 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -1,12 +1,6 @@ - - - - - + + + diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml new file mode 100644 index 0000000..d0bac0f --- /dev/null +++ b/android/app/src/main/res/values-night-v31/styles.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml index 06952be..dbc9ea9 100644 --- a/android/app/src/main/res/values-night/styles.xml +++ b/android/app/src/main/res/values-night/styles.xml @@ -5,6 +5,10 @@ @drawable/launch_background + false + false + false + shortEdges + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index cb1ef88..0d1fa8f 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -5,6 +5,10 @@ @drawable/launch_background + false + false + false + shortEdges - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c2851..0000000 --- a/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist deleted file mode 100644 index 4080f15..0000000 --- a/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Bobmoo Android - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - bobmoo_android - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a5..0000000 --- a/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 86a7c3b..0000000 --- a/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Flutter -import UIKit -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/lib/screens/app_gate.dart b/lib/screens/app_gate.dart index cac6b9e..cb1edf7 100644 --- a/lib/screens/app_gate.dart +++ b/lib/screens/app_gate.dart @@ -1,4 +1,5 @@ import 'package:bobmoo/providers/univ_provider.dart'; +import 'package:bobmoo/screens/loading_screen.dart'; import 'package:bobmoo/screens/splash_screen.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -11,25 +12,30 @@ class AppGate extends StatefulWidget { } class _AppGateState extends State { + static const Duration _minimumSplashDuration = Duration(milliseconds: 1000); + // 한번만 실행되게 가드역할 bool _redirected = false; + bool _isSplashVisible = true; @override - void didChangeDependencies() { - super.didChangeDependencies(); - - final univProvider = context.watch(); + void initState() { + super.initState(); + Future.delayed(_minimumSplashDuration, () { + if (!mounted) return; + setState(() => _isSplashVisible = false); + _tryRedirect(); + }); + } - if (_redirected) return; + void _tryRedirect() { + if (_redirected || _isSplashVisible) return; - if (!univProvider.isInitialized) { - // 아직 초기화 전이면 UI만 보여주고 대기 - return; - } + final univProvider = context.read(); + if (!univProvider.isInitialized) return; _redirected = true; - // build 후에 이동 WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; @@ -45,6 +51,18 @@ class _AppGateState extends State { @override Widget build(BuildContext context) { - return const SplashScreen(); + final univProvider = context.watch(); + _tryRedirect(); + + if (_isSplashVisible) { + return const SplashScreen(); + } + + if (!univProvider.isInitialized) { + return const LoadingScreen(); + } + + // 라우트 이동 직전 프레임 + return const SizedBox.shrink(); } } diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 3538099..7e25b9d 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -41,6 +41,13 @@ class _HomeScreenState extends State with WidgetsBindingObserver { /// 선택한 날짜 저장할 상태 변수 DateTime _selectedDate = DateTime.now(); + static const double _swipeTriggerDistance = 80; + static const double _maxVisualDragOffset = 90; + + double _horizontalDragOffset = 0; + bool _isHorizontalDragging = false; + int _dateTransitionDirection = 1; // 1: 다음날(왼쪽 스와이프), -1: 이전날 + /// 화면이 처음 나타날 때 데이터 불러오기 @override void initState() { @@ -243,6 +250,14 @@ class _HomeScreenState extends State with WidgetsBindingObserver { }); } + void _changeSelectedDateByDays(int days) { + setState(() { + _dateTransitionDirection = days >= 0 ? 1 : -1; + _selectedDate = _selectedDate.add(Duration(days: days)); + _mealFuture = _fetchData(); + }); + } + /// Pull-to-Refresh(당겨서 새로고침)을 위한 새로고침 함수 Future _refreshMeals() async { setState(() { @@ -280,8 +295,11 @@ class _HomeScreenState extends State with WidgetsBindingObserver { lastDate: DateTime(2030), ); if (picked != null && picked != _selectedDate) { - _selectedDate = picked; - _loadMeals(); // 새 날짜로 데이터 로드 + setState(() { + _dateTransitionDirection = picked.isAfter(_selectedDate) ? 1 : -1; + _selectedDate = picked; + _mealFuture = _fetchData(); + }); } } @@ -498,53 +516,125 @@ class _HomeScreenState extends State with WidgetsBindingObserver { return FutureBuilder>( future: _mealFuture, builder: (context, snapshot) { - return CustomScrollView( - physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics(), - ), - slivers: [ - CupertinoSliverRefreshControl( - onRefresh: _refreshMeals, - builder: - ( - context, - refreshState, - pulledExtent, - refreshTriggerPullDistance, - refreshIndicatorExtent, - ) { - return Container( - margin: const EdgeInsets.all(20), - padding: EdgeInsets.all(33.h), - child: CupertinoActivityIndicator( - radius: 10.r, - ), - ); - }, + final currentDateKey = DateFormat('yyyy-MM-dd').format(_selectedDate); + final beginX = _dateTransitionDirection >= 0 ? 0.22 : -0.22; + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onHorizontalDragStart: (_) { + setState(() { + _isHorizontalDragging = true; + }); + }, + onHorizontalDragUpdate: (details) { + setState(() { + _horizontalDragOffset = (_horizontalDragOffset + details.delta.dx) + .clamp(-_maxVisualDragOffset, _maxVisualDragOffset); + }); + }, + onHorizontalDragCancel: () { + setState(() { + _isHorizontalDragging = false; + _horizontalDragOffset = 0; + }); + }, + onHorizontalDragEnd: (_) { + final dragOffset = _horizontalDragOffset; + int? dayDelta; + + if (dragOffset.abs() >= _swipeTriggerDistance) { + dayDelta = dragOffset < 0 ? 1 : -1; // 왼쪽 스와이프=다음 날 + } + + setState(() { + _isHorizontalDragging = false; + _horizontalDragOffset = 0; + }); + + if (dayDelta != null) { + _changeSelectedDateByDays(dayDelta); + } + }, + child: AnimatedContainer( + duration: _isHorizontalDragging + ? Duration.zero + : const Duration(milliseconds: 180), + curve: Curves.easeOutCubic, + transform: Matrix4.translationValues(_horizontalDragOffset, 0, 0), + child: ClipRect( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 260), + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeOutCubic, + transitionBuilder: (child, animation) { + final isIncoming = child.key == ValueKey(currentDateKey); + final slide = isIncoming + ? Tween( + begin: Offset(beginX, 0), + end: Offset.zero, + ).animate(animation) + : Tween( + begin: Offset.zero, + end: Offset(-beginX, 0), + ).animate(ReverseAnimation(animation)); + + return FadeTransition( + opacity: animation, + child: SlideTransition(position: slide, child: child), + ); + }, + child: CustomScrollView( + key: ValueKey(currentDateKey), + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + slivers: [ + CupertinoSliverRefreshControl( + onRefresh: _refreshMeals, + builder: + ( + context, + refreshState, + pulledExtent, + refreshTriggerPullDistance, + refreshIndicatorExtent, + ) { + return Container( + margin: const EdgeInsets.all(20), + padding: EdgeInsets.all(33.h), + child: CupertinoActivityIndicator( + radius: 10.r, + ), + ); + }, + ), + // 케이스별로 다른 Sliver 추가 + // 로딩 중 + if (snapshot.connectionState == ConnectionState.waiting) + SliverFillRemaining( + hasScrollBody: false, + child: const Center(child: CircularProgressIndicator()), + ) + // 에러 발생 + else if (snapshot.hasError) + SliverFillRemaining( + hasScrollBody: false, + child: _buildErrorWidget(snapshot.error!), + ) + // 데이터 없을 시 비어있음 표시 + else if (!snapshot.hasData || snapshot.data!.isEmpty) + SliverFillRemaining( + hasScrollBody: false, + child: _buildEmptyState(), + ) + // 데이터 로딩 성공 -> MealList 위젯 생성 + else + _buildMealList(snapshot.data!), // Sliver 직접 추가 + ], + ), + ), ), - // 케이스별로 다른 Sliver 추가 - // 로딩 중 - if (snapshot.connectionState == ConnectionState.waiting) - SliverFillRemaining( - hasScrollBody: false, - child: const Center(child: CircularProgressIndicator()), - ) - // 에러 발생 - else if (snapshot.hasError) - SliverFillRemaining( - hasScrollBody: false, - child: _buildErrorWidget(snapshot.error!), - ) - // 데이터 없을 시 비어있음 표시 - else if (!snapshot.hasData || snapshot.data!.isEmpty) - SliverFillRemaining( - hasScrollBody: false, - child: _buildEmptyState(), - ) - // 데이터 로딩 성공 -> MealList 위젯 생성 - else - _buildMealList(snapshot.data!), // Sliver 직접 추가 - ], + ), ); }, ); @@ -588,7 +678,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { ), padding: EdgeInsets.symmetric( horizontal: 12.w, - vertical: 1.h, + vertical: 3.h, ), minimumSize: Size.zero, // 최소 사이즈 제거 tapTargetSize: MaterialTapTargetSize.shrinkWrap, // 탭 영역을 최소화 diff --git a/lib/screens/loading_screen.dart b/lib/screens/loading_screen.dart new file mode 100644 index 0000000..bfd1cf5 --- /dev/null +++ b/lib/screens/loading_screen.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class LoadingScreen extends StatelessWidget { + const LoadingScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } +} diff --git a/lib/screens/select_school_screen.dart b/lib/screens/select_school_screen.dart index 9c2ba63..73daa02 100644 --- a/lib/screens/select_school_screen.dart +++ b/lib/screens/select_school_screen.dart @@ -1,5 +1,6 @@ import 'package:bobmoo/models/university.dart'; import 'package:bobmoo/providers/search_provider.dart'; +import 'package:bobmoo/providers/univ_provider.dart'; import 'package:bobmoo/ui/components/buttons/primary_button.dart'; import 'package:bobmoo/ui/theme/app_colors.dart'; import 'package:bobmoo/ui/theme/app_shadow.dart'; @@ -42,6 +43,12 @@ class _SelectSchoolScreenState extends State { ); } + @override + void initState() { + super.initState(); + _selectedUniv = context.read().selectedUniversity; + } + @override Widget build(BuildContext context) { final univs = context.select((SearchProvider p) => p.filteredItems); @@ -107,6 +114,7 @@ class _SelectSchoolScreenState extends State { final university = univs[index]; return ListTile( + contentPadding: EdgeInsets.symmetric(horizontal: 3.w), minTileHeight: 58.h, trailing: _selectedUniv == university ? Icon(Icons.check, size: 25.h) @@ -126,8 +134,7 @@ class _SelectSchoolScreenState extends State { ); }, separatorBuilder: (context, index) => Divider( - // TODO: 두께 들쭉날쭉 관련 논의 - thickness: 2.5, + thickness: 1, color: AppColors.colorGray5, ), ), diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index ff5c89b..a8bb98b 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -1,13 +1,70 @@ +import 'package:bobmoo/ui/theme/app_typography.dart'; import 'package:flutter/material.dart'; +import 'package:bobmoo/ui/theme/app_colors.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_svg/svg.dart'; -class SplashScreen extends StatelessWidget { +class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _fadeAnimation; + late final Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 900), + ); + _fadeAnimation = CurvedAnimation( + parent: _controller, + curve: Curves.easeOutCubic, + ); + _scaleAnimation = Tween(begin: 0.97, end: 1.0).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeOut), + ); + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: AppColors.colorWhite, body: Center( - child: CircularProgressIndicator(), + child: FadeTransition( + opacity: _fadeAnimation, + child: ScaleTransition( + scale: _scaleAnimation, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + 'assets/icons/icon_bob.svg', + width: 107.w, + ), + SizedBox(height: 14.h), + Text( + "밥묵자", + style: AppTypography.head.b48, + ), + ], + ), + ), + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index 4a366d7..252b39c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.0" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" archive: dependency: transitive description: @@ -185,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -315,6 +331,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_native_splash: + dependency: "direct dev" + description: + name: flutter_native_splash + sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002" + url: "https://pub.dev" + source: hosted + version: "2.4.7" flutter_screenutil: dependency: "direct main" description: @@ -381,6 +405,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.8.1" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" http: dependency: "direct main" description: @@ -874,6 +906,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 + url: "https://pub.dev" + source: hosted + version: "2.3.1" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 13f520f..0101630 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,7 @@ dev_dependencies: flutter_lints: ^6.0.0 flutter_launcher_icons: ^0.14.4 + flutter_native_splash: ^2.4.6 build_runner: any isar_community_generator: @@ -88,4 +89,11 @@ flutter_launcher_icons: remove_alpha_ios: true adaptive_icon_background: "#FFFFFF" - adaptive_icon_foreground: "assets/icons/icon.png" \ No newline at end of file + adaptive_icon_foreground: "assets/icons/icon.png" + +flutter_native_splash: + color: "#FFFFFF" + android: true + ios: false + android_12: + color: "#FFFFFF" \ No newline at end of file