이벤트 가져오기 위젯 테스트 추가 및 테스트 훅 도입
This commit is contained in:
@@ -7,7 +7,6 @@ import '../../utils/url_utils.dart';
|
||||
import '../../utils/test_utils.dart';
|
||||
|
||||
import '../../models/event_model.dart';
|
||||
import '../../services/auth_service.dart';
|
||||
import '../../services/event_service.dart';
|
||||
import '../../widgets/loading_indicator.dart';
|
||||
|
||||
@@ -26,8 +25,8 @@ class EventFormScreen extends StatefulWidget {
|
||||
class _EventFormScreenState extends State<EventFormScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isLoading = false;
|
||||
bool _isInit = false;
|
||||
bool _obscurePassword = true; // 비밀번호 숨김 상태 관리
|
||||
bool _testSaveMode = false; // 테스트 훅에서 저장 시 네비/스낵바 우회
|
||||
final _currencyFormat = NumberFormat.currency(locale: 'ko_KR', symbol: '₩', decimalDigits: 0);
|
||||
|
||||
// 폼 필드 컨트롤러
|
||||
@@ -114,6 +113,13 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
// 새 이벤트 생성 시 기본값 설정
|
||||
_type = _typeOptions[0];
|
||||
_status = _statusOptions[0];
|
||||
// 공개 해시를 동기로 초기화하여 초기 렌더 타이밍 변동을 줄임
|
||||
if (_publicHashController.text.isEmpty && !_isHashGenerated) {
|
||||
_isHashGenerated = true;
|
||||
final hash = UrlUtils.generatePublicHash();
|
||||
_publicHashController.text = hash;
|
||||
// 스낵바는 테스트에서 불필요하므로 생략, 일반 환경에서도 최초 자동 생성 시에는 표시하지 않음
|
||||
}
|
||||
}
|
||||
|
||||
// 참가비 입력 컨트롤러에 리스너 추가
|
||||
@@ -122,21 +128,7 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
|
||||
bool _isHashGenerated = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
// 새 이벤트 생성 시에만 해시 자동 생성 (한 번만 실행되도록 플래그 사용)
|
||||
if (widget.event == null && _publicHashController.text.isEmpty && !_isHashGenerated) {
|
||||
_isHashGenerated = true;
|
||||
// 다음 프레임에서 해시 생성 (context 사용 문제 방지)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_generatePublicHash();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// didChangeDependencies 사용 시 프레임 콜백에 의한 타이밍 변동이 생겨 테스트 플래키를 유발할 수 있어 제거
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -238,7 +230,6 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
|
||||
try {
|
||||
final eventService = Provider.of<EventService>(context, listen: false);
|
||||
final authService = Provider.of<AuthService>(context, listen: false);
|
||||
|
||||
// 이벤트 데이터 준비 - null 값 처리 개선
|
||||
final eventData = {
|
||||
@@ -271,47 +262,44 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
if (widget.event == null) {
|
||||
// 새 이벤트 생성
|
||||
await eventService.createEvent(eventData);
|
||||
if (mounted) {
|
||||
// 테스트 환경에서 안전하게 SnackBar 표시
|
||||
if (TestUtils.isInTest) {
|
||||
debugPrint('이벤트가 생성되었습니다');
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('이벤트가 생성되었습니다'),
|
||||
action: SnackBarAction(
|
||||
label: '확인',
|
||||
onPressed: () {},
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
if (mounted && !_testSaveMode) {
|
||||
// 테스트가 아니면 스낵바 표시
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('이벤트가 생성되었습니다'),
|
||||
action: SnackBarAction(
|
||||
label: '확인',
|
||||
onPressed: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 기존 이벤트 수정
|
||||
await eventService.updateEvent(widget.event!.id, eventData);
|
||||
if (mounted) {
|
||||
// 테스트 환경에서 안전하게 SnackBar 표시
|
||||
if (TestUtils.isInTest) {
|
||||
debugPrint('이벤트가 수정되었습니다');
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('이벤트가 수정되었습니다'),
|
||||
action: SnackBarAction(
|
||||
label: '확인',
|
||||
onPressed: () {},
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
if (mounted && !_testSaveMode) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('이벤트가 수정되었습니다'),
|
||||
action: SnackBarAction(
|
||||
label: '확인',
|
||||
onPressed: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(true); // 성공 결과와 함께 이전 화면으로 돌아가기
|
||||
// 성공 후 로딩 해제 (테스트/일반 공통)
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
if (!_testSaveMode) {
|
||||
Navigator.of(context).pop(true); // 성공 결과와 함께 이전 화면으로 돌아가기
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
@@ -331,6 +319,17 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// 테스트 편의를 위한 공개 훅: 버튼 탭 없이 저장 플로우 직접 호출
|
||||
@visibleForTesting
|
||||
Future<void> invokeSaveForTest() async {
|
||||
_testSaveMode = true;
|
||||
try {
|
||||
await _saveEvent();
|
||||
} finally {
|
||||
_testSaveMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 테스트 환경에서 애니메이션 비활성화
|
||||
@@ -427,6 +426,7 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
|
||||
// 이벤트 유형
|
||||
DropdownButtonFormField<String>(
|
||||
key: const Key('event_form_type_dropdown'),
|
||||
value: _type,
|
||||
decoration: InputDecoration(
|
||||
labelText: '이벤트 유형 *',
|
||||
@@ -463,6 +463,7 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
isExpanded: true,
|
||||
items: _typeOptions
|
||||
.map((type) => DropdownMenuItem<String>(
|
||||
value: type,
|
||||
@@ -477,13 +478,15 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: _typeColors[type]?.withOpacity(0.2),
|
||||
color: _typeColors[type]?.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: _typeColors[type] ?? Colors.grey),
|
||||
),
|
||||
child: Text(
|
||||
type,
|
||||
style: TextStyle(color: _typeColors[type]),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -510,12 +513,12 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
margin: const EdgeInsets.only(left: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _typeColors[_type]?.withOpacity(0.2),
|
||||
color: _typeColors[_type]?.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: _typeColors[_type] ?? Colors.grey),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -548,6 +551,7 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
|
||||
// 상태
|
||||
DropdownButtonFormField<String>(
|
||||
key: const Key('event_form_status_dropdown'),
|
||||
value: _status,
|
||||
decoration: InputDecoration(
|
||||
labelText: '상태 *',
|
||||
@@ -584,6 +588,7 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
isExpanded: true,
|
||||
items: _statusOptions
|
||||
.map((status) => DropdownMenuItem<String>(
|
||||
value: status,
|
||||
@@ -598,13 +603,15 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: _statusColors[status]?.withOpacity(0.2),
|
||||
color: _statusColors[status]?.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: _statusColors[status] ?? Colors.grey),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: TextStyle(color: _statusColors[status]),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -631,12 +638,12 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
margin: const EdgeInsets.only(left: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _statusColors[_status]?.withOpacity(0.2),
|
||||
color: _statusColors[_status]?.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: _statusColors[_status] ?? Colors.grey),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -665,6 +672,58 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 공개 접근 섹션
|
||||
_buildSectionTitle('공개 접근'),
|
||||
Builder(
|
||||
builder: (ctx) {
|
||||
final hasHash = _publicHashController.text.trim().isNotEmpty;
|
||||
final publicUrl = hasHash
|
||||
? UrlUtils.getEventPublicUrl(_publicHashController.text.trim())
|
||||
: '';
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: TextEditingController(text: publicUrl),
|
||||
readOnly: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '공개 URL',
|
||||
hintText: '해시 생성 시 공개 URL이 표시됩니다',
|
||||
prefixIcon: Icon(Icons.link),
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: hasHash
|
||||
? () {
|
||||
final url = UrlUtils.getEventPublicUrl(_publicHashController.text.trim());
|
||||
UrlUtils.copyUrlToClipboard(context, url);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.copy),
|
||||
label: const Text('URL 복사'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _generatePublicHash,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: Text(hasHash ? '재생성' : '생성'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 날짜 및 시간 섹션
|
||||
_buildSectionTitle('날짜 및 시간'),
|
||||
|
||||
@@ -721,6 +780,7 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
);
|
||||
|
||||
if (date != null) {
|
||||
if (!context.mounted) return;
|
||||
final time = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(_startDate),
|
||||
@@ -775,6 +835,7 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
);
|
||||
|
||||
if (date != null) {
|
||||
if (!context.mounted) return;
|
||||
final time = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: _endDate != null
|
||||
@@ -830,6 +891,7 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
);
|
||||
|
||||
if (date != null) {
|
||||
if (!context.mounted) return;
|
||||
final time = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: _registrationDeadline != null
|
||||
@@ -1087,14 +1149,10 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
),
|
||||
Tooltip(
|
||||
message: _publicHashController.text.isEmpty ? '새 해시 생성' : '해시 재생성',
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
label: Text(_publicHashController.text.isEmpty ? '생성' : '재생성'),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
color: Colors.blue,
|
||||
onPressed: _generatePublicHash,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1104,9 +1162,9 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
color: Colors.blue.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.withOpacity(0.3)),
|
||||
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -1117,7 +1175,7 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'https://bowling.example.com/events/${_publicHashController.text}',
|
||||
UrlUtils.getEventPublicUrl(_publicHashController.text),
|
||||
style: TextStyle(color: Colors.blue[700]),
|
||||
),
|
||||
),
|
||||
@@ -1147,6 +1205,7 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
key: const ValueKey('access_password_field'),
|
||||
controller: _accessPasswordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '접근 비밀번호 (선택)',
|
||||
@@ -1236,6 +1295,7 @@ class _EventFormScreenState extends State<EventFormScreen> {
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
key: const ValueKey('save_event_button'),
|
||||
onPressed: _saveEvent,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
|
||||
Reference in New Issue
Block a user