feat: 초기 프로젝트 설정 및 LunchPick 앱 구현

LunchPick(오늘 뭐 먹Z?) Flutter 앱의 초기 구현입니다.

주요 기능:
- 네이버 지도 연동 맛집 추가
- 랜덤 메뉴 추천 시스템
- 날씨 기반 거리 조정
- 방문 기록 관리
- Bluetooth 맛집 공유
- 다크모드 지원

기술 스택:
- Flutter 3.8.1+
- Riverpod 상태 관리
- Hive 로컬 DB
- Clean Architecture

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
JiWoong Sul
2025-07-30 19:03:28 +09:00
commit 85fde36157
237 changed files with 30953 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lunchpick/presentation/pages/restaurant_list/widgets/add_restaurant_dialog.dart';
void main() {
group('AddRestaurantDialog Test', () {
testWidgets('다이얼로그에 탭이 표시되는지 확인', (WidgetTester tester) async {
await tester.pumpWidget(
const ProviderScope(
child: MaterialApp(
home: Scaffold(
body: AddRestaurantDialog(),
),
),
),
);
// 탭이 표시되는지 확인
expect(find.text('직접 입력'), findsOneWidget);
expect(find.text('네이버 지도에서 가져오기'), findsOneWidget);
});
testWidgets('네이버 지도 탭으로 전환이 되는지 확인', (WidgetTester tester) async {
await tester.pumpWidget(
const ProviderScope(
child: MaterialApp(
home: Scaffold(
body: AddRestaurantDialog(),
),
),
),
);
// 네이버 지도 탭 클릭
await tester.tap(find.text('네이버 지도에서 가져오기'));
await tester.pumpAndSettle();
// URL 입력 필드가 표시되는지 확인
expect(find.text('네이버 지도 URL'), findsOneWidget);
expect(find.text('가져오기'), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,226 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lunchpick/core/constants/app_constants.dart';
import 'package:lunchpick/core/constants/app_colors.dart';
// 테스트용 SplashScreen - 네비게이션 없음
class TestSplashScreen extends StatefulWidget {
const TestSplashScreen({super.key});
@override
State<TestSplashScreen> createState() => _TestSplashScreenState();
}
class _TestSplashScreenState extends State<TestSplashScreen>
with TickerProviderStateMixin {
late List<AnimationController> _foodControllers;
late AnimationController _questionMarkController;
late AnimationController _centerIconController;
final List<IconData> foodIcons = [
Icons.rice_bowl,
Icons.ramen_dining,
Icons.lunch_dining,
Icons.fastfood,
Icons.local_pizza,
Icons.cake,
Icons.coffee,
Icons.icecream,
Icons.bakery_dining,
];
@override
void initState() {
super.initState();
_initializeAnimations();
// 네비게이션 제거
}
void _initializeAnimations() {
_foodControllers = List.generate(
foodIcons.length,
(index) => AnimationController(
duration: Duration(seconds: 2 + index % 3),
vsync: this,
)..repeat(reverse: true),
);
_questionMarkController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
)..repeat();
_centerIconController = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
)..repeat(reverse: true);
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDark ? AppColors.darkBackground : AppColors.lightBackground,
body: Stack(
children: [
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ScaleTransition(
scale: Tween(begin: 0.8, end: 1.2).animate(
CurvedAnimation(
parent: _centerIconController,
curve: Curves.easeInOut,
),
),
child: Icon(
Icons.restaurant_menu,
size: 80,
color: isDark ? AppColors.darkPrimary : AppColors.lightPrimary,
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'오늘 뭐 먹Z',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: isDark ? AppColors.darkTextPrimary : AppColors.lightTextPrimary,
),
),
AnimatedBuilder(
animation: _questionMarkController,
builder: (context, child) {
final questionMarks = '?' * (((_questionMarkController.value * 3).floor() % 3) + 1);
return Text(
questionMarks,
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: isDark ? AppColors.darkTextPrimary : AppColors.lightTextPrimary,
),
);
},
),
],
),
],
),
),
Positioned(
bottom: 30,
left: 0,
right: 0,
child: Text(
AppConstants.appCopyright,
style: TextStyle(
fontSize: 12,
color: (isDark ? AppColors.darkTextSecondary : AppColors.lightTextSecondary)
.withValues(alpha: 0.5),
),
textAlign: TextAlign.center,
),
),
],
),
);
}
@override
void dispose() {
for (final controller in _foodControllers) {
controller.dispose();
}
_questionMarkController.dispose();
_centerIconController.dispose();
super.dispose();
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('LunchPickApp 위젯 테스트', () {
testWidgets('스플래시 화면이 올바르게 표시되는지 확인', (WidgetTester tester) async {
// 테스트용 스플래시 화면 사용
await tester.pumpWidget(
const MaterialApp(
home: TestSplashScreen(),
),
);
// 스플래시 화면 요소 확인
expect(find.text('오늘 뭐 먹Z'), findsOneWidget);
expect(find.byIcon(Icons.restaurant_menu), findsOneWidget);
expect(find.text(AppConstants.appCopyright), findsOneWidget);
// 애니메이션이 있으므로 pump를 여러 번 호출
await tester.pump(const Duration(seconds: 1));
// 여전히 스플래시 화면에 있는지 확인
expect(find.text('오늘 뭐 먹Z'), findsOneWidget);
});
testWidgets('스플래시 화면 물음표 애니메이션 확인', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: TestSplashScreen(),
),
);
// 초기 상태에서 물음표가 포함된 텍스트 확인
expect(find.textContaining('오늘 뭐 먹Z'), findsOneWidget);
// 애니메이션 진행
await tester.pump(const Duration(milliseconds: 500));
// 여전히 제목이 표시되는지 확인
expect(find.textContaining('오늘 뭐 먹Z'), findsOneWidget);
});
testWidgets('스플래시 화면 라이트 테마 색상 확인', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
brightness: Brightness.light,
primaryColor: AppColors.lightPrimary,
scaffoldBackgroundColor: AppColors.lightBackground,
),
home: const TestSplashScreen(),
),
);
// BuildContext 가져오기
final BuildContext context = tester.element(find.byType(TestSplashScreen));
final theme = Theme.of(context);
// 라이트 테마 확인
expect(theme.brightness, Brightness.light);
});
testWidgets('스플래시 화면 다크 테마 색상 확인', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: AppColors.darkPrimary,
scaffoldBackgroundColor: AppColors.darkBackground,
),
home: const TestSplashScreen(),
),
);
// BuildContext 가져오기
final BuildContext context = tester.element(find.byType(TestSplashScreen));
final theme = Theme.of(context);
// 다크 테마 확인
expect(theme.brightness, Brightness.dark);
});
});
}