이벤트 가져오기 위젯 테스트 추가 및 테스트 훅 도입
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../models/participant_model.dart';
|
||||
import '../../services/event_service.dart';
|
||||
import '../../widgets/loading_indicator.dart';
|
||||
import '../../utils/enum_mappings.dart';
|
||||
|
||||
class ParticipantFormScreen extends StatefulWidget {
|
||||
final String eventId;
|
||||
@@ -35,13 +35,22 @@ class _ParticipantFormScreenState extends State<ParticipantFormScreen> {
|
||||
String? _status;
|
||||
bool _isPaid = false;
|
||||
|
||||
// 참가자 상태 옵션
|
||||
final List<String> _statusOptions = ['등록됨', '확인됨', '취소됨', '참석함'];
|
||||
// 참가자 상태 옵션 (enum_mappings 사용)
|
||||
late final List<String> _statusKoOptions;
|
||||
late final Map<String, String> _statusEnToKo;
|
||||
late final Map<String, String> _statusKoToEn;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// enum 매핑 구성
|
||||
_statusEnToKo = Map<String, String>.from(participantStatusOptions);
|
||||
_statusKoToEn = {
|
||||
for (final e in participantStatusOptions.entries) e.value: e.key,
|
||||
};
|
||||
_statusKoOptions = participantStatusOptions.values.toList(growable: false);
|
||||
|
||||
// 수정 모드인 경우 기존 데이터 로드
|
||||
if (widget.participant != null) {
|
||||
_nameController.text = widget.participant!.name ?? '';
|
||||
@@ -50,11 +59,14 @@ class _ParticipantFormScreenState extends State<ParticipantFormScreen> {
|
||||
_notesController.text = widget.participant!.notes ?? '';
|
||||
_paidAmountController.text = widget.participant!.paidAmount?.toString() ?? '';
|
||||
|
||||
_status = widget.participant!.status;
|
||||
// 서버/모델 상태(영문)를 UI 드롭다운(한글)으로 변환
|
||||
final currentStatusEn = widget.participant!.status ?? 'pending';
|
||||
final currentStatusKo = _statusEnToKo[currentStatusEn] ?? _statusKoOptions.first;
|
||||
_status = _statusKoOptions.contains(currentStatusKo) ? currentStatusKo : _statusKoOptions.first;
|
||||
_isPaid = widget.participant!.isPaid;
|
||||
} else {
|
||||
// 새 참가자 추가 시 기본값 설정
|
||||
_status = _statusOptions[0];
|
||||
_status = _statusKoOptions.first; // pending
|
||||
_isPaid = false;
|
||||
}
|
||||
}
|
||||
@@ -88,8 +100,11 @@ class _ParticipantFormScreenState extends State<ParticipantFormScreen> {
|
||||
'name': _nameController.text.trim(),
|
||||
'email': _emailController.text.trim().isEmpty ? null : _emailController.text.trim(),
|
||||
'phoneNumber': _phoneNumberController.text.trim().isEmpty ? null : _phoneNumberController.text.trim(),
|
||||
'status': _status,
|
||||
// 서버는 영문 상태를 기대, UI는 한글 상태를 사용하므로 변환 (pending/confirmed/canceled)
|
||||
'status': _statusKoToEn[_status ?? _statusKoOptions.first] ?? 'pending',
|
||||
// 결제 상태는 enum 키 사용 (unpaid/paid)
|
||||
'isPaid': _isPaid,
|
||||
'paymentStatus': _isPaid ? 'paid' : 'unpaid',
|
||||
'paidAmount': _paidAmountController.text.isEmpty ? null : double.parse(_paidAmountController.text),
|
||||
'notes': _notesController.text.trim().isEmpty ? null : _notesController.text.trim(),
|
||||
};
|
||||
@@ -149,9 +164,18 @@ class _ParticipantFormScreenState extends State<ParticipantFormScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 게스트 사전입력 버튼
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _showGuestPrefillSheet,
|
||||
icon: const Icon(Icons.person_add),
|
||||
label: const Text('게스트 사전입력'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 기본 정보 섹션
|
||||
_buildSectionTitle('기본 정보'),
|
||||
|
||||
// 이름
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
@@ -167,7 +191,6 @@ class _ParticipantFormScreenState extends State<ParticipantFormScreen> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 이메일
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
@@ -178,7 +201,6 @@ class _ParticipantFormScreenState extends State<ParticipantFormScreen> {
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
// 간단한 이메일 형식 검증
|
||||
if (!value.contains('@') || !value.contains('.')) {
|
||||
return '유효한 이메일 주소를 입력해주세요';
|
||||
}
|
||||
@@ -187,7 +209,6 @@ class _ParticipantFormScreenState extends State<ParticipantFormScreen> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 전화번호
|
||||
TextFormField(
|
||||
controller: _phoneNumberController,
|
||||
@@ -198,22 +219,22 @@ class _ParticipantFormScreenState extends State<ParticipantFormScreen> {
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 상태 섹션
|
||||
_buildSectionTitle('상태 정보'),
|
||||
|
||||
// 참가자 상태
|
||||
DropdownButtonFormField<String>(
|
||||
value: _status,
|
||||
key: const Key('participant_form_status_dropdown'),
|
||||
value: _statusKoOptions.contains(_status) ? _status : _statusKoOptions.first,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '참가자 상태',
|
||||
),
|
||||
items: _statusOptions.map((status) {
|
||||
return DropdownMenuItem(
|
||||
value: status,
|
||||
child: Text(status),
|
||||
);
|
||||
}).toList(),
|
||||
isExpanded: true,
|
||||
items: _statusKoOptions
|
||||
.map((status) => DropdownMenuItem<String>(
|
||||
value: status,
|
||||
child: Text(status, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_status = value;
|
||||
@@ -221,7 +242,6 @@ class _ParticipantFormScreenState extends State<ParticipantFormScreen> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 결제 여부
|
||||
SwitchListTile(
|
||||
title: const Text('결제 완료'),
|
||||
@@ -232,7 +252,6 @@ class _ParticipantFormScreenState extends State<ParticipantFormScreen> {
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
// 결제 금액
|
||||
TextFormField(
|
||||
controller: _paidAmountController,
|
||||
@@ -252,10 +271,8 @@ class _ParticipantFormScreenState extends State<ParticipantFormScreen> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 메모 섹션
|
||||
_buildSectionTitle('추가 정보'),
|
||||
|
||||
// 메모
|
||||
TextFormField(
|
||||
controller: _notesController,
|
||||
@@ -266,7 +283,6 @@ class _ParticipantFormScreenState extends State<ParticipantFormScreen> {
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 저장 버튼
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -288,6 +304,92 @@ class _ParticipantFormScreenState extends State<ParticipantFormScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// 게스트 사전입력 BottomSheet
|
||||
void _showGuestPrefillSheet() {
|
||||
final nameCtrl = TextEditingController(text: _nameController.text);
|
||||
final emailCtrl = TextEditingController(text: _emailController.text);
|
||||
final phoneCtrl = TextEditingController(text: _phoneNumberController.text);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (ctx) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: MediaQuery.of(ctx).viewInsets.bottom + 16,
|
||||
top: 16,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('게스트 사전입력', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: nameCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '이름 *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: emailCtrl,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '이메일',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: phoneCtrl,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '전화번호',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (nameCtrl.text.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||
const SnackBar(content: Text('이름은 필수 입력 항목입니다')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_nameController.text = nameCtrl.text.trim();
|
||||
_emailController.text = emailCtrl.text.trim();
|
||||
_phoneNumberController.text = phoneCtrl.text.trim();
|
||||
});
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
child: const Text('적용'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 섹션 제목 위젯
|
||||
Widget _buildSectionTitle(String title) {
|
||||
return Column(
|
||||
|
||||
Reference in New Issue
Block a user