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