이벤트 가져오기 위젯 테스트 추가 및 테스트 훅 도입

This commit is contained in:
2025-09-12 06:33:44 +09:00
parent 107abc963f
commit a5f2544d11
121 changed files with 39277 additions and 3911 deletions
+125 -60
View File
@@ -16,6 +16,7 @@ import 'screens/club/members_screen.dart';
import 'screens/club/events_screen.dart';
import 'screens/club/club_settings_screen.dart';
import 'screens/score/club_statistics_screen.dart';
import 'widgets/dialog_actions.dart';
// 전역 네비게이터 키 (인증 오류 처리용)
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@@ -58,10 +59,82 @@ class MyApp extends StatelessWidget {
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
visualDensity: VisualDensity.adaptivePlatformDensity,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
chipTheme: ChipThemeData(
shape: StadiumBorder(side: BorderSide.none),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 0),
),
dialogTheme: const DialogThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
titleTextStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black87),
contentTextStyle: TextStyle(fontSize: 14, color: Colors.black87),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: const StadiumBorder(),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: const StadiumBorder(),
),
),
cardTheme: const CardThemeData(
elevation: 1.5,
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 6),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
isDense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
),
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8))),
actionTextColor: Colors.white,
),
),
darkTheme: ThemeData(
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark),
useMaterial3: true,
visualDensity: VisualDensity.adaptivePlatformDensity,
textTheme: const TextTheme(
bodyMedium: TextStyle(color: Colors.white70),
bodySmall: TextStyle(color: Colors.white60),
labelMedium: TextStyle(color: Colors.white70),
),
iconTheme: IconThemeData(
color: Colors.grey.shade200, // 다크에서 아이콘 대비 상향
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: const StadiumBorder(),
side: BorderSide(color: Colors.grey.shade400), // 다크에서 border 대비 강화
),
),
cardTheme: const CardThemeData(
elevation: 0.5, // 다크에서 쉐도우 약화
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 6),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8))),
),
),
// 로케일 설정 추가
localizationsDelegates: const [
@@ -112,17 +185,24 @@ class _HomeScreenState extends State<HomeScreen> {
});
}
// 클럽 선택 다이얼로그 표시
Future<void> _showClubSelectionDialog(BuildContext context) async {
final clubService = Provider.of<ClubService>(context, listen: false);
final authService = Provider.of<AuthService>(context, listen: false);
// 클럽 선택 다이얼로그 표시 (context를 인자로 받지 않음)
Future<void> _showClubSelectionDialog() async {
// 현재 시점의 컨텍스트를 통해 의존 객체를 가져오되, BuildContext 자체는 보관하지 않는다.
final currentContext = navigatorKey.currentContext!;
final clubService = Provider.of<ClubService>(currentContext, listen: false);
final authService = Provider.of<AuthService>(currentContext, listen: false);
final messenger = ScaffoldMessenger.of(currentContext);
// onTap 내부에서 사용할 서비스들을 await 이전에 캡처
final memberService = Provider.of<MemberService>(currentContext, listen: false);
final eventService = Provider.of<EventService>(currentContext, listen: false);
final scoreService = Provider.of<ScoreService>(currentContext, listen: false);
// 사용자의 모든 클럽 가져오기
if (clubService.clubs.isEmpty) {
try {
await clubService.fetchUserClubs();
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
messenger.showSnackBar(
SnackBar(content: Text('클럽 목록을 불러오는데 실패했습니다: $e')),
);
return;
@@ -130,18 +210,16 @@ class _HomeScreenState extends State<HomeScreen> {
}
if (clubService.clubs.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
messenger.showSnackBar(
const SnackBar(content: Text('소속된 클럽이 없습니다')),
);
return;
}
if (!context.mounted) return;
// 다이얼로그 표시
showDialog(
context: context,
builder: (BuildContext context) {
context: navigatorKey.currentContext!,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Text('클럽 선택'),
content: SizedBox(
@@ -158,7 +236,7 @@ class _HomeScreenState extends State<HomeScreen> {
subtitle: Text(club.description ?? ''),
trailing: isSelected ? const Icon(Icons.check, color: Colors.blue) : null,
onTap: () async {
Navigator.of(context).pop();
Navigator.of(dialogContext).pop();
if (!isSelected) {
try {
@@ -166,9 +244,6 @@ class _HomeScreenState extends State<HomeScreen> {
await clubService.selectClub(club.id);
// 관련 서비스 초기화
final memberService = Provider.of<MemberService>(context, listen: false);
final eventService = Provider.of<EventService>(context, listen: false);
final scoreService = Provider.of<ScoreService>(context, listen: false);
memberService.initialize(authService.token!);
eventService.initialize(authService.token!);
@@ -180,17 +255,13 @@ class _HomeScreenState extends State<HomeScreen> {
eventService.fetchClubEvents(),
]);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${club.name} 클럽으로 변경되었습니다')),
);
}
messenger.showSnackBar(
SnackBar(content: Text('${club.name} 클럽으로 변경되었습니다')),
);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('클럽 변경 중 오류가 발생했습니다: $e')),
);
}
messenger.showSnackBar(
SnackBar(content: Text('클럽 변경 중 오류가 발생했습니다: $e')),
);
}
}
},
@@ -198,12 +269,11 @@ class _HomeScreenState extends State<HomeScreen> {
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('취소'),
),
],
actions: DialogActions.confirm(
onCancel: () => Navigator.of(dialogContext).pop(),
onConfirm: () => Navigator.of(dialogContext).pop(),
confirmText: '닫기',
),
);
},
);
@@ -219,6 +289,8 @@ class _HomeScreenState extends State<HomeScreen> {
}
Future<void> _initializeServices() async {
// messenger는 try/catch 전역에서 재사용 (await 이후 context 접근 회피)
final messenger = ScaffoldMessenger.of(context);
try {
final authService = Provider.of<AuthService>(context, listen: false);
final clubService = Provider.of<ClubService>(context, listen: false);
@@ -242,42 +314,33 @@ class _HomeScreenState extends State<HomeScreen> {
await clubService.selectClub(clubService.clubs[0].id);
scoreService.initialize(authService.token!);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${clubService.clubs[0].name} 클럽이 자동으로 선택되었습니다')),
);
}
messenger.showSnackBar(
SnackBar(content: Text('${clubService.clubs[0].name} 클럽이 자동으로 선택되었습니다')),
);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('클럽 자동 선택 중 오류가 발생했습니다: $e')),
);
}
messenger.showSnackBar(
SnackBar(content: Text('클럽 자동 선택 중 오류가 발생했습니다: $e')),
);
}
} else if (clubService.clubs.isNotEmpty) {
// 클럽이 여러 개 있으면 선택 다이얼로그 표시 (타이머 생성 회피: microtask 사용)
if (!mounted) return;
Future.microtask(() {
if (context.mounted) {
_showClubSelectionDialog(context);
}
_showClubSelectionDialog();
});
} else {
// 클럽이 없는 경우 메시지 표시
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('소속된 클럽이 없습니다. 클럽을 생성하거나 초대를 요청하세요.')),
);
}
messenger.showSnackBar(
const SnackBar(content: Text('소속된 클럽이 없습니다. 클럽을 생성하거나 초대를 요청하세요.')),
);
}
}
}
} catch (e) {
// 오류 처리
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('서비스 초기화 중 오류가 발생했습니다: $e')),
);
}
messenger.showSnackBar(
SnackBar(content: Text('서비스 초기화 중 오류가 발생했습니다: $e')),
);
}
}
@@ -288,7 +351,7 @@ class _HomeScreenState extends State<HomeScreen> {
title: Consumer<ClubService>(
builder: (context, clubService, child) {
return InkWell(
onTap: () => _showClubSelectionDialog(context),
onTap: () => _showClubSelectionDialog(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -309,15 +372,17 @@ class _HomeScreenState extends State<HomeScreen> {
onPressed: () async {
// 알림 권한 요청
final notificationService = NotificationService();
final messenger = ScaffoldMessenger.of(context);
final hasPermission = await notificationService.requestPermission();
if (context.mounted) {
if (hasPermission) {
ScaffoldMessenger.of(context).showSnackBar(
if (hasPermission) {
if (context.mounted) {
messenger.showSnackBar(
const SnackBar(content: Text('알림 권한이 허용되었습니다')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
}
} else {
if (context.mounted) {
messenger.showSnackBar(
const SnackBar(content: Text('알림 권한이 거부되었습니다. 설정에서 권한을 허용해주세요')),
);
}