이벤트 가져오기 위젯 테스트 추가 및 테스트 훅 도입
This commit is contained in:
+125
-60
@@ -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('알림 권한이 거부되었습니다. 설정에서 권한을 허용해주세요')),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user