LCOV - code coverage report
Current view: top level - lib/screens/club - event_form_screen.dart Coverage Total Hit
Test: lcov.info Lines: 64.3 % 594 382
Test Date: 2025-08-12 03:21:51 Functions: - 0 0

            Line data    Source code
       1              : import 'package:flutter/material.dart';
       2              : import 'package:flutter/services.dart';
       3              : import 'package:provider/provider.dart';
       4              : import 'package:intl/intl.dart';
       5              : 
       6              : import '../../utils/url_utils.dart';
       7              : import '../../utils/test_utils.dart';
       8              : 
       9              : import '../../models/event_model.dart';
      10              : import '../../services/auth_service.dart';
      11              : import '../../services/event_service.dart';
      12              : import '../../widgets/loading_indicator.dart';
      13              : 
      14              : class EventFormScreen extends StatefulWidget {
      15              :   final Event? event; // null이면 새 이벤트 생성, 아니면 이벤트 수정
      16              : 
      17           25 :   const EventFormScreen({
      18              :     Key? key,
      19              :     this.event,
      20            1 :   }) : super(key: key);
      21              : 
      22            1 :   @override
      23            1 :   State<EventFormScreen> createState() => _EventFormScreenState();
      24              : }
      25              : 
      26              : class _EventFormScreenState extends State<EventFormScreen> {
      27              :   final _formKey = GlobalKey<FormState>();
      28              :   bool _isLoading = false;
      29              :   bool _isInit = false;
      30              :   bool _obscurePassword = true; // 비밀번호 숨김 상태 관리
      31              :   final _currencyFormat = NumberFormat.currency(locale: 'ko_KR', symbol: '₩', decimalDigits: 0);
      32              : 
      33              :   // 폼 필드 컨트롤러
      34              :   final TextEditingController _titleController = TextEditingController();
      35              :   final TextEditingController _descriptionController = TextEditingController();
      36              :   final TextEditingController _locationController = TextEditingController();
      37              :   final TextEditingController _maxParticipantsController = TextEditingController();
      38              :   final TextEditingController _gameCountController = TextEditingController();
      39              :   final TextEditingController _participantFeeController = TextEditingController();
      40              :   final TextEditingController _formattedFeeController = TextEditingController(); // 포맷된 참가비 표시용
      41              :   final TextEditingController _publicHashController = TextEditingController();
      42              :   final TextEditingController _accessPasswordController = TextEditingController();
      43              : 
      44              :   // 이벤트 데이터
      45              :   String? _type;
      46              :   String? _status;
      47              :   DateTime _startDate = DateTime.now();
      48              :   DateTime? _endDate;
      49              :   DateTime? _registrationDeadline;
      50              : 
      51              :   // 이벤트 유형 및 상태 옵션
      52              :   final List<String> _typeOptions = ['개인전', '팀전', '리그', '토너먼트', '연습', '기타'];
      53              :   final List<String> _statusOptions = ['활성', '대기', '취소', '완료'];
      54              :   
      55              :   // 이벤트 유형 및 상태별 색상
      56              :   final Map<String, Color> _typeColors = {
      57              :     '개인전': Colors.blue,
      58              :     '팀전': Colors.green,
      59              :     '리그': Colors.purple,
      60              :     '토너먼트': Colors.orange,
      61              :     '연습': Colors.teal,
      62              :     '기타': Colors.grey,
      63              :   };
      64              :   
      65              :   final Map<String, Color> _statusColors = {
      66              :     '활성': Colors.green,
      67              :     '대기': Colors.amber,
      68              :     '취소': Colors.red,
      69              :     '완료': Colors.blue,
      70              :   };
      71              :   
      72              :   // 이벤트 유형 및 상태별 아이콘
      73              :   final Map<String, IconData> _typeIcons = {
      74              :     '개인전': Icons.person,
      75              :     '팀전': Icons.group,
      76              :     '리그': Icons.sports,
      77              :     '토너먼트': Icons.emoji_events,
      78              :     '연습': Icons.fitness_center,
      79              :     '기타': Icons.category,
      80              :   };
      81              :   
      82              :   final Map<String, IconData> _statusIcons = {
      83              :     '활성': Icons.check_circle,
      84              :     '대기': Icons.hourglass_empty,
      85              :     '취소': Icons.cancel,
      86              :     '완료': Icons.done_all,
      87              :   };
      88              : 
      89            1 :   @override
      90              :   void initState() {
      91            1 :     super.initState();
      92              :     
      93            2 :     if (widget.event != null) {
      94              :       // 기존 이벤트 수정 시 필드 초기화
      95            0 :       _titleController.text = widget.event!.title;
      96            0 :       _descriptionController.text = widget.event!.description ?? '';
      97            0 :       _locationController.text = widget.event!.location ?? '';
      98            0 :       _maxParticipantsController.text = widget.event!.maxParticipants?.toString() ?? '';
      99            0 :       _gameCountController.text = widget.event!.gameCount?.toString() ?? '';
     100            0 :       _publicHashController.text = widget.event!.publicHash ?? '';
     101            0 :       _accessPasswordController.text = widget.event!.accessPassword ?? '';
     102              :       
     103            0 :       _type = widget.event!.type;
     104            0 :       _status = widget.event!.status;
     105            0 :       _startDate = widget.event!.startDate;
     106            0 :       _endDate = widget.event!.endDate;
     107            0 :       _registrationDeadline = widget.event!.registrationDeadline;
     108              :       
     109              :       // 참가비가 있으면 포맷된 참가비 컨트롤러 초기화
     110            0 :       if (widget.event!.participantFee != null) {
     111            0 :         _updateFormattedFee(widget.event!.participantFee.toString());
     112              :       }
     113              :     } else {
     114              :       // 새 이벤트 생성 시 기본값 설정
     115            3 :       _type = _typeOptions[0];
     116            3 :       _status = _statusOptions[0];
     117              :     }
     118              :     
     119              :     // 참가비 입력 컨트롤러에 리스너 추가
     120            3 :     _participantFeeController.addListener(_onParticipantFeeChanged);
     121              :   }
     122              :   
     123              :   bool _isHashGenerated = false;
     124              :   
     125            1 :   @override
     126              :   void didChangeDependencies() {
     127            1 :     super.didChangeDependencies();
     128              :     
     129              :     // 새 이벤트 생성 시에만 해시 자동 생성 (한 번만 실행되도록 플래그 사용)
     130            6 :     if (widget.event == null && _publicHashController.text.isEmpty && !_isHashGenerated) {
     131            1 :       _isHashGenerated = true;
     132              :       // 다음 프레임에서 해시 생성 (context 사용 문제 방지)
     133            3 :       WidgetsBinding.instance.addPostFrameCallback((_) {
     134            1 :         if (mounted) {
     135            1 :           _generatePublicHash();
     136              :         }
     137              :       });
     138              :     }
     139              :   }
     140              : 
     141            1 :   @override
     142              :   void dispose() {
     143            2 :     _titleController.dispose();
     144            2 :     _descriptionController.dispose();
     145            2 :     _locationController.dispose();
     146            2 :     _maxParticipantsController.dispose();
     147            2 :     _gameCountController.dispose();
     148            3 :     _participantFeeController.removeListener(_onParticipantFeeChanged);
     149            2 :     _participantFeeController.dispose();
     150            2 :     _formattedFeeController.dispose();
     151            2 :     _publicHashController.dispose();
     152            2 :     _accessPasswordController.dispose();
     153            1 :     super.dispose();
     154              :   }
     155              : 
     156              :   // 공개 URL 해시 생성
     157            1 :   void _generatePublicHash() {
     158              :     // 이전 해시 저장
     159            2 :     final previousHash = _publicHashController.text;
     160            1 :     final isNew = previousHash.isEmpty;
     161              :     
     162              :     // 새 해시 생성
     163            1 :     final hash = UrlUtils.generatePublicHash();
     164              :     
     165            2 :     setState(() {
     166            2 :       _publicHashController.text = hash;
     167              :     });
     168              :     
     169              :     // 해시 생성 후 사용자에게 피드백 제공
     170              :     // 테스트 환경에서는 로그만 출력하고, 일반 환경에서만 스낵바 표시
     171            1 :     if (mounted) {
     172            1 :       if (TestUtils.isInTest) {
     173            3 :         debugPrint('해시 생성 완료: $hash');
     174              :       } else {
     175              :         // 일반 환경에서는 다음 프레임에서 스낵바 표시
     176            0 :         WidgetsBinding.instance.addPostFrameCallback((_) {
     177            0 :           if (mounted) {
     178              :             try {
     179            0 :               final scaffoldMessenger = ScaffoldMessenger.of(context);
     180              :               final message = isNew
     181              :                   ? '공개 URL 해시가 생성되었습니다'
     182              :                   : '공개 URL 해시가 재생성되었습니다. 이전 URL은 더 이상 유효하지 않습니다';
     183              :               
     184            0 :               scaffoldMessenger.showSnackBar(
     185            0 :                 SnackBar(
     186            0 :                   content: Text(message),
     187            0 :                   action: SnackBarAction(
     188              :                     label: 'URL 복사',
     189            0 :                     onPressed: () {
     190            0 :                       final url = UrlUtils.getEventPublicUrl(hash);
     191            0 :                       UrlUtils.copyUrlToClipboard(context, url);
     192              :                     },
     193              :                   ),
     194              :                 ),
     195              :               );
     196              :             } catch (e) {
     197            0 :               debugPrint('스낵바 표시 오류: $e');
     198              :             }
     199              :           }
     200              :         });
     201              :       }
     202              :     }
     203              :   }
     204              : 
     205              :   // 이벤트 저장
     206            1 :   Future<void> _saveEvent() async {
     207            3 :     final isValid = _formKey.currentState!.validate();
     208              :     
     209              :     if (!isValid) {
     210              :       // 유효성 검사 실패 시 스낵바로 알림
     211            1 :       if (mounted) {
     212              :         // 테스트 환경에서 안전하게 SnackBar 표시
     213            1 :         if (TestUtils.isInTest) {
     214            2 :           debugPrint('유효성 검사 실패: 필수 입력 항목을 모두 작성해주세요');
     215              :         } else {
     216              :           // 일반 환경에서는 SnackBar 표시
     217            0 :           WidgetsBinding.instance.addPostFrameCallback((_) {
     218            0 :             if (mounted) {
     219            0 :               final scaffoldMessenger = ScaffoldMessenger.of(context);
     220            0 :               scaffoldMessenger.hideCurrentSnackBar();
     221            0 :               scaffoldMessenger.showSnackBar(
     222              :                 const SnackBar(
     223              :                   content: Text('필수 입력 항목을 모두 작성해주세요 (제목, 이벤트 유형, 상태, 시작 날짜)'),
     224              :                   backgroundColor: Colors.red,
     225              :                   duration: Duration(seconds: 3),
     226              :                 ),
     227              :               );
     228              :             }
     229              :           });
     230              :         }
     231              :       }
     232              :       return;
     233              :     }
     234              :     
     235            0 :     setState(() {
     236            0 :       _isLoading = true;
     237              :     });
     238              :     
     239              :     try {
     240            0 :       final eventService = Provider.of<EventService>(context, listen: false);
     241            0 :       final authService = Provider.of<AuthService>(context, listen: false);
     242              :       
     243              :       // 이벤트 데이터 준비 - null 값 처리 개선
     244            0 :       final eventData = {
     245            0 :         'title': _titleController.text.trim(),
     246            0 :         'description': _descriptionController.text.trim().isEmpty ? null : _descriptionController.text.trim(),
     247            0 :         'location': _locationController.text.trim().isEmpty ? null : _locationController.text.trim(),
     248            0 :         'type': _type,
     249            0 :         'status': _status,
     250            0 :         'startDate': _startDate.toIso8601String(),
     251            0 :         'endDate': _endDate?.toIso8601String(),
     252            0 :         'maxParticipants': _maxParticipantsController.text.isEmpty ? null : int.parse(_maxParticipantsController.text),
     253            0 :         'gameCount': _gameCountController.text.isEmpty ? null : int.parse(_gameCountController.text),
     254            0 :         'participantFee': _participantFeeController.text.isEmpty ? null : double.parse(_participantFeeController.text),
     255            0 :         'registrationDeadline': _registrationDeadline?.toIso8601String(),
     256            0 :         'publicHash': _publicHashController.text.trim().isEmpty ? null : _publicHashController.text.trim(),
     257            0 :         'accessPassword': _accessPasswordController.text.trim().isEmpty ? null : _accessPasswordController.text.trim(),
     258              :       };
     259              :       
     260              :       // 서버에 명시적으로 null 값을 전송하기 위해 빈 문자열을 null로 변환
     261              :       // JavaScript에서 || 연산자로 인한 문제 방지
     262            0 :       eventData.forEach((key, value) {
     263            0 :         if (value is String && value.isEmpty) {
     264            0 :           eventData[key] = null;
     265              :         }
     266              :       });
     267              :       
     268              :       // 디버그용 로그 - 개발 완료 후 제거 필요
     269            0 :       debugPrint('Event data to send: $eventData');
     270              :       
     271            0 :       if (widget.event == null) {
     272              :         // 새 이벤트 생성
     273            0 :         await eventService.createEvent(eventData);
     274            0 :         if (mounted) {
     275              :           // 테스트 환경에서 안전하게 SnackBar 표시
     276            0 :           if (TestUtils.isInTest) {
     277            0 :             debugPrint('이벤트가 생성되었습니다');
     278              :           } else {
     279            0 :             ScaffoldMessenger.of(context).showSnackBar(
     280            0 :               SnackBar(
     281              :                 content: const Text('이벤트가 생성되었습니다'),
     282            0 :                 action: SnackBarAction(
     283              :                   label: '확인',
     284            0 :                   onPressed: () {},
     285              :                 ),
     286              :                 duration: const Duration(seconds: 3),
     287              :               ),
     288              :             );
     289              :           }
     290              :         }
     291              :       } else {
     292              :         // 기존 이벤트 수정
     293            0 :         await eventService.updateEvent(widget.event!.id, eventData);
     294            0 :         if (mounted) {
     295              :           // 테스트 환경에서 안전하게 SnackBar 표시
     296            0 :           if (TestUtils.isInTest) {
     297            0 :             debugPrint('이벤트가 수정되었습니다');
     298              :           } else {
     299            0 :             ScaffoldMessenger.of(context).showSnackBar(
     300            0 :               SnackBar(
     301              :                 content: const Text('이벤트가 수정되었습니다'),
     302            0 :                 action: SnackBarAction(
     303              :                   label: '확인',
     304            0 :                   onPressed: () {},
     305              :                 ),
     306              :                 duration: const Duration(seconds: 3),
     307              :               ),
     308              :             );
     309              :           }
     310              :         }
     311              :       }
     312              :       
     313            0 :       if (mounted) {
     314            0 :         Navigator.of(context).pop(true); // 성공 결과와 함께 이전 화면으로 돌아가기
     315              :       }
     316              :     } catch (e) {
     317            0 :       if (mounted) {
     318            0 :         setState(() {
     319            0 :           _isLoading = false;
     320              :         });
     321              :         
     322              :         // 테스트 환경에서 안전하게 SnackBar 표시
     323            0 :         if (TestUtils.isInTest) {
     324            0 :           debugPrint('이벤트 저장에 실패했습니다: $e');
     325              :         } else {
     326            0 :           ScaffoldMessenger.of(context).showSnackBar(
     327            0 :             SnackBar(content: Text('이벤트 저장에 실패했습니다: $e')),
     328              :           );
     329              :         }
     330              :       }
     331              :     }
     332              :   }
     333              : 
     334            1 :   @override
     335              :   Widget build(BuildContext context) {
     336              :     // 테스트 환경에서 애니메이션 비활성화
     337            1 :     final Widget scaffold = Scaffold(
     338            1 :       appBar: AppBar(
     339            3 :         title: Text(widget.event == null ? '새 이벤트 생성' : '이벤트 수정'),
     340            1 :         actions: [
     341            1 :           IconButton(
     342              :             icon: const Icon(Icons.save),
     343            1 :             onPressed: _saveEvent,
     344              :           ),
     345              :         ],
     346              :       ),
     347            1 :       body: _isLoading
     348              :           ? const LoadingIndicator()
     349            1 :           : SingleChildScrollView(
     350              :               padding: const EdgeInsets.all(16.0),
     351            1 :               child: Form(
     352            1 :                 key: _formKey,
     353            1 :                 child: Column(
     354              :                   crossAxisAlignment: CrossAxisAlignment.start,
     355            1 :                   children: [
     356              :                     // 기본 정보 섹션
     357            1 :                     _buildSectionTitle('기본 정보'),
     358              :                     
     359              :                     // 제목
     360            1 :                     TextFormField(
     361            1 :                       controller: _titleController,
     362            1 :                       decoration: InputDecoration(
     363              :                         labelText: '이벤트 제목 *',
     364              :                         hintText: '이벤트 제목을 입력하세요',
     365              :                         prefixIcon: const Icon(Icons.title),
     366              :                         // 필수 필드 표시 강화
     367            1 :                         labelStyle: TextStyle(
     368            4 :                           color: _titleController.text.trim().isEmpty ? Colors.red : Colors.blue,
     369              :                           fontWeight: FontWeight.bold,
     370              :                         ),
     371              :                         // 입력 상태에 따른 테두리 색상 변경
     372            1 :                         enabledBorder: OutlineInputBorder(
     373            1 :                           borderSide: BorderSide(
     374            4 :                             color: _titleController.text.trim().isEmpty ? Colors.grey : Colors.green,
     375              :                             width: 1.0,
     376              :                           ),
     377              :                         ),
     378              :                         focusedBorder: const OutlineInputBorder(
     379              :                           borderSide: BorderSide(
     380              :                             color: Colors.blue,
     381              :                             width: 2.0,
     382              :                           ),
     383              :                         ),
     384              :                         // 오류 시 테두리 색상
     385              :                         errorBorder: const OutlineInputBorder(
     386              :                           borderSide: BorderSide(
     387              :                             color: Colors.red,
     388              :                             width: 1.0,
     389              :                           ),
     390              :                         ),
     391              :                         // 입력 상태에 따른 도움말 텍스트
     392            4 :                         helperText: _titleController.text.trim().isNotEmpty
     393              :                             ? '유효한 제목입니다'
     394              :                             : '이벤트의 제목을 입력해주세요 (필수)',
     395            1 :                         helperStyle: TextStyle(
     396            4 :                           color: _titleController.text.trim().isEmpty ? Colors.grey : Colors.green,
     397            4 :                           fontStyle: _titleController.text.trim().isEmpty ? FontStyle.italic : FontStyle.normal,
     398              :                         ),
     399              :                         // 입력 완료 시 체크 아이콘 표시
     400            4 :                         suffixIcon: _titleController.text.trim().isNotEmpty
     401              :                             ? const Icon(Icons.check_circle, color: Colors.green)
     402              :                             : null,
     403              :                       ),
     404            1 :                       validator: (value) {
     405            2 :                         if (value == null || value.trim().isEmpty) {
     406              :                           return '이벤트 제목을 입력해주세요';
     407              :                         }
     408              :                         return null;
     409              :                       },
     410            0 :                       onChanged: (value) {
     411              :                         // 입력 변경 시 실시간으로 UI 업데이트
     412            0 :                         setState(() {});
     413              :                       },
     414              :                     ),
     415              :                     const SizedBox(height: 16),
     416              :                     
     417              :                     // 설명
     418            1 :                     TextFormField(
     419            1 :                       controller: _descriptionController,
     420              :                       decoration: const InputDecoration(
     421              :                         labelText: '이벤트 설명',
     422              :                         hintText: '이벤트 설명을 입력하세요',
     423              :                       ),
     424              :                       maxLines: 3,
     425              :                     ),
     426              :                     const SizedBox(height: 16),
     427              :                     
     428              :                     // 이벤트 유형
     429            1 :                     DropdownButtonFormField<String>(
     430            1 :                       value: _type,
     431            1 :                       decoration: InputDecoration(
     432              :                         labelText: '이벤트 유형 *',
     433            1 :                         labelStyle: TextStyle(
     434            1 :                           color: _type == null ? Colors.red : Colors.blue,
     435              :                           fontWeight: FontWeight.bold,
     436              :                         ),
     437              :                         prefixIcon: const Icon(Icons.category),
     438            1 :                         helperText: _type != null
     439              :                             ? '유효한 이벤트 유형입니다'
     440              :                             : '이벤트 유형을 선택해주세요 (필수)',
     441            1 :                         helperStyle: TextStyle(
     442            1 :                           color: _type == null ? Colors.grey : Colors.green,
     443            1 :                           fontStyle: _type == null ? FontStyle.italic : FontStyle.normal,
     444              :                         ),
     445              :                         // 입력 상태에 따른 테두리 색상 변경
     446            1 :                         enabledBorder: OutlineInputBorder(
     447            1 :                           borderSide: BorderSide(
     448            1 :                             color: _type == null ? Colors.grey : Colors.green,
     449              :                             width: 1.0,
     450              :                           ),
     451              :                         ),
     452              :                         focusedBorder: const OutlineInputBorder(
     453              :                           borderSide: BorderSide(
     454              :                             color: Colors.blue,
     455              :                             width: 2.0,
     456              :                           ),
     457              :                         ),
     458              :                         // 오류 시 테두리 색상
     459              :                         errorBorder: const OutlineInputBorder(
     460              :                           borderSide: BorderSide(
     461              :                             color: Colors.red,
     462              :                             width: 1.0,
     463              :                           ),
     464              :                         ),
     465              :                       ),
     466            1 :                       items: _typeOptions
     467            3 :                           .map((type) => DropdownMenuItem<String>(
     468              :                                 value: type,
     469            1 :                                 child: Row(
     470            1 :                                   children: [
     471            1 :                                     Icon(
     472            2 :                                       _typeIcons[type] ?? Icons.category,
     473            2 :                                       color: _typeColors[type],
     474              :                                       size: 18,
     475              :                                     ),
     476              :                                     const SizedBox(width: 8),
     477            1 :                                     Container(
     478              :                                       padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
     479            1 :                                       decoration: BoxDecoration(
     480            3 :                                         color: _typeColors[type]?.withOpacity(0.2),
     481            1 :                                         borderRadius: BorderRadius.circular(12),
     482            3 :                                         border: Border.all(color: _typeColors[type] ?? Colors.grey),
     483              :                                       ),
     484            1 :                                       child: Text(
     485              :                                         type,
     486            3 :                                         style: TextStyle(color: _typeColors[type]),
     487              :                                       ),
     488              :                                     ),
     489              :                                   ],
     490              :                                 ),
     491              :                               ))
     492            1 :                           .toList(),
     493            0 :                       onChanged: (value) {
     494            0 :                         setState(() {
     495            0 :                           _type = value;
     496              :                         });
     497              :                       },
     498            1 :                       validator: (value) {
     499            1 :                         if (value == null || value.isEmpty) {
     500              :                           return '이벤트 유형을 선택해주세요';
     501              :                         }
     502              :                         return null;
     503              :                       },
     504              :                     ),
     505              :                     const SizedBox(height: 8),
     506            1 :                     if (_type != null)
     507            1 :                       Align(
     508              :                         alignment: Alignment.centerLeft,
     509            1 :                         child: Container(
     510              :                           margin: const EdgeInsets.only(left: 12),
     511              :                           padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
     512            1 :                           decoration: BoxDecoration(
     513            4 :                             color: _typeColors[_type]?.withOpacity(0.2),
     514            1 :                             borderRadius: BorderRadius.circular(12),
     515            4 :                             border: Border.all(color: _typeColors[_type] ?? Colors.grey),
     516            1 :                             boxShadow: [
     517            1 :                               BoxShadow(
     518            1 :                                 color: Colors.black.withOpacity(0.05),
     519              :                                 blurRadius: 2,
     520              :                                 offset: const Offset(0, 1),
     521              :                               ),
     522              :                             ],
     523              :                           ),
     524            1 :                           child: Row(
     525              :                             mainAxisSize: MainAxisSize.min,
     526            1 :                             children: [
     527            1 :                               Icon(
     528            3 :                                 _typeIcons[_type] ?? Icons.category,
     529            3 :                                 color: _typeColors[_type],
     530              :                                 size: 14,
     531              :                               ),
     532              :                               const SizedBox(width: 4),
     533            1 :                               Text(
     534            1 :                                 _type!,
     535            1 :                                 style: TextStyle(
     536            3 :                                   color: _typeColors[_type], 
     537              :                                   fontSize: 12,
     538              :                                   fontWeight: FontWeight.bold,
     539              :                                 ),
     540              :                               ),
     541              :                             ],
     542              :                           ),
     543              :                         ),
     544              :                       ),
     545            1 :                     const SizedBox(height: 16),
     546              :                     
     547            1 :                     const SizedBox(height: 16),
     548              :                     
     549              :                     // 상태
     550            1 :                     DropdownButtonFormField<String>(
     551            1 :                       value: _status,
     552            1 :                       decoration: InputDecoration(
     553              :                         labelText: '상태 *',
     554            1 :                         labelStyle: TextStyle(
     555            1 :                           color: _status == null ? Colors.red : Colors.blue,
     556              :                           fontWeight: FontWeight.bold,
     557              :                         ),
     558              :                         prefixIcon: const Icon(Icons.info_outline),
     559            1 :                         helperText: _status != null
     560              :                             ? '유효한 상태입니다'
     561              :                             : '이벤트 상태를 선택해주세요 (필수)',
     562            1 :                         helperStyle: TextStyle(
     563            1 :                           color: _status == null ? Colors.grey : Colors.green,
     564            1 :                           fontStyle: _status == null ? FontStyle.italic : FontStyle.normal,
     565              :                         ),
     566              :                         // 입력 상태에 따른 테두리 색상 변경
     567            1 :                         enabledBorder: OutlineInputBorder(
     568            1 :                           borderSide: BorderSide(
     569            1 :                             color: _status == null ? Colors.grey : Colors.green,
     570              :                             width: 1.0,
     571              :                           ),
     572              :                         ),
     573              :                         focusedBorder: const OutlineInputBorder(
     574              :                           borderSide: BorderSide(
     575              :                             color: Colors.blue,
     576              :                             width: 2.0,
     577              :                           ),
     578              :                         ),
     579              :                         // 오류 시 테두리 색상
     580              :                         errorBorder: const OutlineInputBorder(
     581              :                           borderSide: BorderSide(
     582              :                             color: Colors.red,
     583              :                             width: 1.0,
     584              :                           ),
     585              :                         ),
     586              :                       ),
     587            1 :                       items: _statusOptions
     588            3 :                           .map((status) => DropdownMenuItem<String>(
     589              :                                 value: status,
     590            1 :                                 child: Row(
     591            1 :                                   children: [
     592            1 :                                     Icon(
     593            2 :                                       _statusIcons[status] ?? Icons.info,
     594            2 :                                       color: _statusColors[status],
     595              :                                       size: 18,
     596              :                                     ),
     597              :                                     const SizedBox(width: 8),
     598            1 :                                     Container(
     599              :                                       padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
     600            1 :                                       decoration: BoxDecoration(
     601            3 :                                         color: _statusColors[status]?.withOpacity(0.2),
     602            1 :                                         borderRadius: BorderRadius.circular(12),
     603            3 :                                         border: Border.all(color: _statusColors[status] ?? Colors.grey),
     604              :                                       ),
     605            1 :                                       child: Text(
     606              :                                         status,
     607            3 :                                         style: TextStyle(color: _statusColors[status]),
     608              :                                       ),
     609              :                                     ),
     610              :                                   ],
     611              :                                 ),
     612              :                               ))
     613            1 :                           .toList(),
     614            0 :                       onChanged: (value) {
     615            0 :                         setState(() {
     616            0 :                           _status = value;
     617              :                         });
     618              :                       },
     619            1 :                       validator: (value) {
     620            1 :                         if (value == null || value.isEmpty) {
     621              :                           return '상태를 선택해주세요';
     622              :                         }
     623              :                         return null;
     624              :                       },
     625              :                     ),
     626            1 :                     const SizedBox(height: 8),
     627            1 :                     if (_status != null)
     628            1 :                       Align(
     629              :                         alignment: Alignment.centerLeft,
     630            1 :                         child: Container(
     631              :                           margin: const EdgeInsets.only(left: 12),
     632              :                           padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
     633            1 :                           decoration: BoxDecoration(
     634            4 :                             color: _statusColors[_status]?.withOpacity(0.2),
     635            1 :                             borderRadius: BorderRadius.circular(12),
     636            4 :                             border: Border.all(color: _statusColors[_status] ?? Colors.grey),
     637            1 :                             boxShadow: [
     638            1 :                               BoxShadow(
     639            1 :                                 color: Colors.black.withOpacity(0.05),
     640              :                                 blurRadius: 2,
     641              :                                 offset: const Offset(0, 1),
     642              :                               ),
     643              :                             ],
     644              :                           ),
     645            1 :                           child: Row(
     646              :                             mainAxisSize: MainAxisSize.min,
     647            1 :                             children: [
     648            1 :                               Icon(
     649            3 :                                 _statusIcons[_status] ?? Icons.info,
     650            3 :                                 color: _statusColors[_status],
     651              :                                 size: 14,
     652              :                               ),
     653              :                               const SizedBox(width: 4),
     654            1 :                               Text(
     655            1 :                                 _status!,
     656            1 :                                 style: TextStyle(
     657            3 :                                   color: _statusColors[_status], 
     658              :                                   fontSize: 12,
     659              :                                   fontWeight: FontWeight.bold,
     660              :                                 ),
     661              :                               ),
     662              :                             ],
     663              :                           ),
     664              :                         ),
     665              :                       ),
     666            1 :                     const SizedBox(height: 24),
     667              :                     
     668              :                     // 날짜 및 시간 섹션
     669            1 :                     _buildSectionTitle('날짜 및 시간'),
     670              :                     
     671              :                     // 시작 날짜
     672            1 :                     Container(
     673            1 :                       decoration: BoxDecoration(
     674            1 :                         border: Border.all(
     675              :                           color: Colors.green,
     676              :                           width: 1.0,
     677              :                         ),
     678            1 :                         borderRadius: BorderRadius.circular(4.0),
     679              :                       ),
     680              :                       margin: const EdgeInsets.symmetric(vertical: 8.0),
     681            1 :                       child: ListTile(
     682            1 :                         title: Row(
     683            1 :                           children: [
     684              :                             const Text('시작 날짜 및 시간 *', 
     685              :                               style: TextStyle(
     686              :                                 fontWeight: FontWeight.bold,
     687              :                                 color: Colors.blue,
     688              :                               ),
     689              :                             ),
     690              :                             const SizedBox(width: 8),
     691              :                             const Icon(Icons.check_circle, color: Colors.green, size: 16),
     692              :                           ],
     693              :                         ),
     694            1 :                         subtitle: Column(
     695              :                           crossAxisAlignment: CrossAxisAlignment.start,
     696            1 :                           children: [
     697            1 :                             Text(
     698            3 :                               DateFormat('yyyy년 MM월 dd일 HH:mm').format(_startDate),
     699              :                               style: const TextStyle(fontWeight: FontWeight.bold),
     700              :                             ),
     701              :                             const SizedBox(height: 4),
     702            1 :                             Row(
     703            1 :                               children: [
     704              :                                 const Icon(Icons.info_outline, size: 12, color: Colors.green),
     705              :                                 const SizedBox(width: 4),
     706              :                                 const Text('이벤트 시작 날짜와 시간을 설정해주세요 (필수)',
     707              :                                   style: TextStyle(fontSize: 12, fontStyle: FontStyle.italic, color: Colors.grey),
     708              :                                 ),
     709              :                               ],
     710              :                             ),
     711              :                           ],
     712              :                         ),
     713              :                         leading: const Icon(Icons.event, color: Colors.blue),
     714              :                         trailing: const Icon(Icons.calendar_today),
     715            0 :                         onTap: () async {
     716            0 :                           final date = await showDatePicker(
     717              :                             context: context,
     718            0 :                             initialDate: _startDate,
     719            0 :                             firstDate: DateTime(2020),
     720            0 :                             lastDate: DateTime(2030),
     721              :                           );
     722              :                           
     723              :                           if (date != null) {
     724            0 :                             final time = await showTimePicker(
     725              :                               context: context,
     726            0 :                               initialTime: TimeOfDay.fromDateTime(_startDate),
     727              :                             );
     728              :                             
     729            0 :                             if (time != null && mounted) {
     730            0 :                               setState(() {
     731            0 :                                 _startDate = DateTime(
     732            0 :                                   date.year,
     733            0 :                                   date.month,
     734            0 :                                   date.day,
     735            0 :                                   time.hour,
     736            0 :                                   time.minute,
     737              :                                 );
     738              :                               });
     739              :                             }
     740              :                           }
     741              :                         },
     742              :                       ),
     743              :                     ),
     744            1 :                     const SizedBox(height: 8),
     745              :                     
     746              :                     // 종료 날짜
     747            1 :                     ListTile(
     748              :                       title: const Text('종료 날짜 및 시간 (선택)'),
     749            1 :                       subtitle: _endDate != null
     750            0 :                           ? Text(
     751            0 :                               DateFormat('yyyy년 MM월 dd일 HH:mm').format(_endDate!),
     752              :                             )
     753              :                           : const Text('설정되지 않음'),
     754            1 :                       trailing: Row(
     755              :                         mainAxisSize: MainAxisSize.min,
     756            1 :                         children: [
     757            1 :                           if (_endDate != null)
     758            0 :                             IconButton(
     759              :                               icon: const Icon(Icons.clear),
     760            0 :                               onPressed: () {
     761            0 :                                 setState(() {
     762            0 :                                   _endDate = null;
     763              :                                 });
     764              :                               },
     765              :                             ),
     766            1 :                           const Icon(Icons.calendar_today),
     767              :                         ],
     768              :                       ),
     769            0 :                       onTap: () async {
     770            0 :                         final date = await showDatePicker(
     771              :                           context: context,
     772            0 :                           initialDate: _endDate ?? _startDate.add(const Duration(hours: 2)),
     773            0 :                           firstDate: DateTime(2020),
     774            0 :                           lastDate: DateTime(2030),
     775              :                         );
     776              :                         
     777              :                         if (date != null) {
     778            0 :                           final time = await showTimePicker(
     779              :                             context: context,
     780            0 :                             initialTime: _endDate != null
     781            0 :                                 ? TimeOfDay.fromDateTime(_endDate!)
     782            0 :                                 : TimeOfDay.fromDateTime(_startDate.add(const Duration(hours: 2))),
     783              :                           );
     784              :                           
     785            0 :                           if (time != null && mounted) {
     786            0 :                             setState(() {
     787            0 :                               _endDate = DateTime(
     788            0 :                                 date.year,
     789            0 :                                 date.month,
     790            0 :                                 date.day,
     791            0 :                                 time.hour,
     792            0 :                                 time.minute,
     793              :                               );
     794              :                             });
     795              :                           }
     796              :                         }
     797              :                       },
     798              :                     ),
     799            1 :                     const SizedBox(height: 8),
     800              :                     
     801              :                     // 등록 마감일
     802            1 :                     ListTile(
     803              :                       title: const Text('등록 마감일 (선택)'),
     804            1 :                       subtitle: _registrationDeadline != null
     805            0 :                           ? Text(
     806            0 :                               DateFormat('yyyy년 MM월 dd일 HH:mm').format(_registrationDeadline!),
     807              :                             )
     808              :                           : const Text('설정되지 않음'),
     809            1 :                       trailing: Row(
     810              :                         mainAxisSize: MainAxisSize.min,
     811            1 :                         children: [
     812            1 :                           if (_registrationDeadline != null)
     813            0 :                             IconButton(
     814              :                               icon: const Icon(Icons.clear),
     815            0 :                               onPressed: () {
     816            0 :                                 setState(() {
     817            0 :                                   _registrationDeadline = null;
     818              :                                 });
     819              :                               },
     820              :                             ),
     821            1 :                           const Icon(Icons.calendar_today),
     822              :                         ],
     823              :                       ),
     824            0 :                       onTap: () async {
     825            0 :                         final date = await showDatePicker(
     826              :                           context: context,
     827            0 :                           initialDate: _registrationDeadline ?? _startDate.subtract(const Duration(days: 1)),
     828            0 :                           firstDate: DateTime(2020),
     829            0 :                           lastDate: DateTime(2030),
     830              :                         );
     831              :                         
     832              :                         if (date != null) {
     833            0 :                           final time = await showTimePicker(
     834              :                             context: context,
     835            0 :                             initialTime: _registrationDeadline != null
     836            0 :                                 ? TimeOfDay.fromDateTime(_registrationDeadline!)
     837            0 :                                 : TimeOfDay.fromDateTime(_startDate.subtract(const Duration(days: 1))),
     838              :                           );
     839              :                           
     840            0 :                           if (time != null && mounted) {
     841            0 :                             setState(() {
     842            0 :                               _registrationDeadline = DateTime(
     843            0 :                                 date.year,
     844            0 :                                 date.month,
     845            0 :                                 date.day,
     846            0 :                                 time.hour,
     847            0 :                                 time.minute,
     848              :                               );
     849              :                             });
     850              :                           }
     851              :                         }
     852              :                       },
     853              :                     ),
     854            1 :                     const SizedBox(height: 24),
     855              :                     
     856              :                     // 장소 및 참가자 섹션
     857            1 :                     _buildSectionTitle('장소 및 참가자'),
     858              :                     
     859              :                     // 장소
     860            1 :                     TextFormField(
     861            1 :                       controller: _locationController,
     862              :                       decoration: const InputDecoration(
     863              :                         labelText: '장소',
     864              :                         hintText: '이벤트 장소를 입력하세요',
     865              :                       ),
     866              :                     ),
     867            1 :                     const SizedBox(height: 16),
     868              :                     
     869              :                     // 최대 참가자 수
     870            1 :                     TextFormField(
     871            1 :                       controller: _maxParticipantsController,
     872              :                       decoration: const InputDecoration(
     873              :                         labelText: '최대 참가자 수',
     874              :                         hintText: '최대 참가자 수를 입력하세요',
     875              :                         suffixText: '명',
     876              :                       ),
     877              :                       keyboardType: TextInputType.number,
     878            1 :                       validator: (value) {
     879            1 :                         if (value != null && value.isNotEmpty) {
     880            0 :                           if (int.tryParse(value) == null) {
     881              :                             return '숫자를 입력해주세요';
     882              :                           }
     883              :                         }
     884              :                         return null;
     885              :                       },
     886              :                     ),
     887            1 :                     const SizedBox(height: 16),
     888              :                     
     889              :                     // 게임 수
     890            1 :                     TextFormField(
     891            1 :                       controller: _gameCountController,
     892            1 :                       decoration: InputDecoration(
     893              :                         labelText: '게임 수',
     894              :                         hintText: '게임 수를 입력하세요',
     895              :                         suffixText: '게임',
     896              :                         helperText: '이벤트에서 진행할 게임 수를 입력하세요',
     897            2 :                         helperStyle: TextStyle(color: Colors.grey[600]),
     898              :                       ),
     899              :                       keyboardType: TextInputType.number,
     900            1 :                       inputFormatters: [
     901            1 :                         FilteringTextInputFormatter.digitsOnly,
     902              :                       ],
     903            1 :                       validator: (value) {
     904            1 :                         if (value != null && value.isNotEmpty) {
     905            0 :                           final numValue = int.tryParse(value);
     906              :                           if (numValue == null) {
     907              :                             return '유효한 숫자를 입력해주세요';
     908              :                           }
     909            0 :                           if (numValue <= 0) {
     910              :                             return '1 이상의 숫자를 입력해주세요';
     911              :                           }
     912            0 :                           if (numValue > 100) {
     913              :                             return '게임 수가 너무 많습니다 (100 이하)';
     914              :                           }
     915              :                         }
     916              :                         return null;
     917              :                       },
     918            0 :                       onChanged: (value) {
     919              :                         // 입력 중에도 화면 업데이트를 위해 setState 호출
     920            0 :                         setState(() {});
     921              :                       },
     922              :                     ),
     923              :                     // 게임 수가 유효한 경우 안내 메시지 표시
     924            2 :                     Builder(builder: (context) {
     925            3 :                       if (_gameCountController.text.isNotEmpty && 
     926            0 :                           int.tryParse(_gameCountController.text) != null && 
     927            0 :                           int.parse(_gameCountController.text) > 0) {
     928            0 :                         return Padding(
     929              :                           padding: const EdgeInsets.only(top: 4.0, left: 12.0),
     930            0 :                           child: Text(
     931            0 :                             '총 ${int.parse(_gameCountController.text)} 게임이 진행됩니다',
     932            0 :                             style: TextStyle(color: Colors.green[700], fontSize: 12),
     933              :                           ),
     934              :                         );
     935              :                       } else {
     936              :                         return const SizedBox.shrink();
     937              :                       }
     938              :                     }),
     939            1 :                     const SizedBox(height: 16),
     940              :                     
     941              :                     // 참가비
     942            1 :                     Column(
     943              :                       crossAxisAlignment: CrossAxisAlignment.start,
     944            1 :                       children: [
     945              :                         // 숨겨진 실제 참가비 입력 필드 (데이터 저장용)
     946            1 :                         Offstage(
     947              :                           offstage: true,
     948            1 :                           child: TextFormField(
     949            1 :                             controller: _participantFeeController,
     950              :                             keyboardType: const TextInputType.numberWithOptions(decimal: true),
     951              :                           ),
     952              :                         ),
     953              :                         
     954              :                         // 포맷된 참가비 표시 필드 (사용자 입력용)
     955            1 :                         TextFormField(
     956            1 :                           controller: _formattedFeeController,
     957            1 :                           decoration: InputDecoration(
     958              :                             labelText: '참가비',
     959              :                             hintText: '참가비를 입력하세요',
     960              :                             prefixText: '₩',
     961            1 :                             helperText: _getParticipantFeeHelperText(),
     962            1 :                             helperStyle: TextStyle(
     963            1 :                               color: _getParticipantFeeColor(),
     964            3 :                               fontStyle: _participantFeeController.text.isEmpty ? FontStyle.italic : FontStyle.normal,
     965            3 :                               fontWeight: _participantFeeController.text.isNotEmpty ? FontWeight.bold : FontWeight.normal,
     966              :                             ),
     967              :                             // 입력 상태에 따른 테두리 색상 변경
     968            3 :                             enabledBorder: _participantFeeController.text.isEmpty
     969              :                                 ? null
     970            1 :                                 : OutlineInputBorder(
     971            1 :                                     borderSide: BorderSide(
     972            1 :                                       color: _getParticipantFeeColor(),
     973              :                                       width: 1.5,
     974              :                                     ),
     975              :                                   ),
     976            3 :                             suffixIcon: _formattedFeeController.text.isNotEmpty
     977            1 :                                 ? IconButton(
     978              :                                     icon: const Icon(Icons.clear),
     979            0 :                                     onPressed: () {
     980            0 :                                       setState(() {
     981            0 :                                         _formattedFeeController.clear();
     982            0 :                                         _participantFeeController.clear();
     983              :                                       });
     984              :                                     },
     985              :                                     tooltip: '참가비 지우기',
     986              :                                   )
     987              :                                 : null,
     988              :                           ),
     989              :                           keyboardType: const TextInputType.numberWithOptions(decimal: false),
     990            1 :                           inputFormatters: [
     991            1 :                             FilteringTextInputFormatter.digitsOnly,
     992              :                           ],
     993            1 :                           validator: (value) {
     994            1 :                             if (value != null && value.isNotEmpty) {
     995            0 :                               final numValue = double.tryParse(_participantFeeController.text);
     996              :                               if (numValue == null) {
     997              :                                 return '유효한 금액을 입력해주세요';
     998              :                               }
     999            0 :                               if (numValue < 0) {
    1000              :                                 return '0 이상의 금액을 입력해주세요';
    1001              :                               }
    1002            0 :                               if (numValue > 1000000) {
    1003              :                                 return '금액이 너무 큽니다 (최대 1,000,000원)';
    1004              :                               }
    1005              :                             }
    1006              :                             return null;
    1007              :                           },
    1008            1 :                           onChanged: (value) {
    1009            1 :                             if (value.isNotEmpty) {
    1010              :                               // 콤마 제거 후 숫자만 추출
    1011            2 :                               final plainNumber = value.replaceAll(RegExp(r'[^0-9]'), '');
    1012            1 :                               final numericValue = int.tryParse(plainNumber);
    1013              :                               
    1014              :                               if (numericValue != null) {
    1015              :                                 // 원본 값 저장
    1016            3 :                                 _participantFeeController.text = numericValue.toString();
    1017              :                                 
    1018              :                                 // 포맷된 값 표시 (커서 위치 보존)
    1019            3 :                                 final cursorPosition = _formattedFeeController.selection.baseOffset;
    1020            2 :                                 final oldText = _formattedFeeController.text;
    1021            2 :                                 final newText = _currencyFormat.format(numericValue);
    1022              :                                 
    1023              :                                 // 커서 위치 계산 로직 개선
    1024              :                                 int newCursorPosition;
    1025            1 :                                 if (oldText.isEmpty) {
    1026            0 :                                   newCursorPosition = newText.length;
    1027              :                                 } else {
    1028              :                                   // 이전 텍스트와 새 텍스트의 길이 차이 계산
    1029            3 :                                   final lengthDiff = newText.length - oldText.length;
    1030            3 :                                   newCursorPosition = (cursorPosition + lengthDiff).clamp(0, newText.length);
    1031              :                                 }
    1032              :                                 
    1033            3 :                                 _formattedFeeController.value = TextEditingValue(
    1034              :                                   text: newText,
    1035            1 :                                   selection: TextSelection.collapsed(offset: newCursorPosition),
    1036              :                                 );
    1037              :                               }
    1038              :                             } else {
    1039            0 :                               _participantFeeController.clear();
    1040              :                             }
    1041              :                             
    1042              :                             // UI 업데이트를 위해 setState 호출
    1043            2 :                             setState(() {});
    1044              :                           },
    1045              :                         ),
    1046              :                         
    1047              :                         // 참가비 입력 시 안내 메시지
    1048            3 :                         if (_formattedFeeController.text.isNotEmpty)
    1049            1 :                           Padding(
    1050              :                             padding: const EdgeInsets.only(top: 4.0, left: 12.0),
    1051            1 :                             child: Row(
    1052            1 :                               children: [
    1053            3 :                                 Icon(_getParticipantFeeIcon(), size: 16, color: _getParticipantFeeColor()),
    1054              :                                 const SizedBox(width: 8),
    1055            1 :                                 Text(
    1056            1 :                                   _getParticipantFeeStatusText(), 
    1057            2 :                                   style: TextStyle(color: _getParticipantFeeColor(), fontSize: 12, fontWeight: FontWeight.bold),
    1058              :                                 ),
    1059              :                               ],
    1060              :                             ),
    1061              :                           ),
    1062              :                       ],
    1063              :                     ),
    1064            1 :                     const SizedBox(height: 4),
    1065              :                     
    1066            1 :                     const SizedBox(height: 24),
    1067              :                     
    1068              :                     // 공개 접근 섹션
    1069            1 :                     _buildSectionTitle('공개 접근'),
    1070              :                     
    1071              :                     // 공개 URL 해시
    1072            1 :                     Column(
    1073              :                       crossAxisAlignment: CrossAxisAlignment.start,
    1074            1 :                       children: [
    1075            1 :                         Row(
    1076            1 :                           children: [
    1077            1 :                             Expanded(
    1078            1 :                               child: TextFormField(
    1079            1 :                                 controller: _publicHashController,
    1080            1 :                                 decoration: InputDecoration(
    1081              :                                   labelText: '공개 URL 해시',
    1082              :                                   hintText: '공개 URL 해시를 입력하세요',
    1083              :                                   helperText: '이벤트를 공개적으로 공유하기 위한 고유 식별자입니다',
    1084              :                                 ),
    1085              :                                 readOnly: true,
    1086              :                               ),
    1087              :                             ),
    1088            1 :                             Tooltip(
    1089            3 :                               message: _publicHashController.text.isEmpty ? '새 해시 생성' : '해시 재생성',
    1090            1 :                               child: ElevatedButton.icon(
    1091              :                                 icon: const Icon(Icons.refresh, size: 18),
    1092            4 :                                 label: Text(_publicHashController.text.isEmpty ? '생성' : '재생성'),
    1093            1 :                                 onPressed: _generatePublicHash,
    1094            1 :                                 style: ElevatedButton.styleFrom(
    1095              :                                   backgroundColor: Colors.blue,
    1096              :                                   foregroundColor: Colors.white,
    1097              :                                 ),
    1098              :                               ),
    1099              :                             ),
    1100              :                           ],
    1101              :                         ),
    1102              :                         const SizedBox(height: 8),
    1103            3 :                         if (_publicHashController.text.isNotEmpty)
    1104            1 :                           Container(
    1105              :                             padding: const EdgeInsets.all(12),
    1106            1 :                             decoration: BoxDecoration(
    1107            1 :                               color: Colors.blue.withOpacity(0.1),
    1108            1 :                               borderRadius: BorderRadius.circular(8),
    1109            2 :                               border: Border.all(color: Colors.blue.withOpacity(0.3)),
    1110              :                             ),
    1111            1 :                             child: Column(
    1112              :                               crossAxisAlignment: CrossAxisAlignment.start,
    1113            1 :                               children: [
    1114              :                                 const Text('공개 URL:', style: TextStyle(fontWeight: FontWeight.bold)),
    1115              :                                 const SizedBox(height: 4),
    1116            1 :                                 Row(
    1117            1 :                                   children: [
    1118            1 :                                     Expanded(
    1119            1 :                                       child: Text(
    1120            3 :                                         'https://bowling.example.com/events/${_publicHashController.text}',
    1121            2 :                                         style: TextStyle(color: Colors.blue[700]),
    1122              :                                       ),
    1123              :                                     ),
    1124            1 :                                     IconButton(
    1125              :                                       icon: const Icon(Icons.copy, color: Colors.blue),
    1126            0 :                                       onPressed: () {
    1127            0 :                                         final url = UrlUtils.getEventPublicUrl(_publicHashController.text);
    1128            0 :                                         UrlUtils.copyUrlToClipboard(context, url);
    1129              :                                       },
    1130              :                                       tooltip: 'URL 복사',
    1131              :                                     ),
    1132              :                                   ],
    1133              :                                 ),
    1134              :                                 const SizedBox(height: 4),
    1135              :                                 const Text('이 URL을 공유하면 누구나 이벤트를 볼 수 있습니다.', 
    1136              :                                   style: TextStyle(fontSize: 12, fontStyle: FontStyle.italic)),
    1137              :                               ],
    1138              :                             ),
    1139              :                           ),
    1140              :                       ],
    1141              :                     ),
    1142              :                     
    1143            1 :                     const SizedBox(height: 16),
    1144              :                     
    1145              :                     // 접근 비밀번호
    1146            1 :                     Column(
    1147              :                       crossAxisAlignment: CrossAxisAlignment.start,
    1148            1 :                       children: [
    1149            1 :                         TextFormField(
    1150            1 :                           controller: _accessPasswordController,
    1151            1 :                           decoration: InputDecoration(
    1152              :                             labelText: '접근 비밀번호 (선택)',
    1153              :                             hintText: '접근 비밀번호를 입력하세요',
    1154            1 :                             helperText: _getPasswordHelperText(),
    1155            1 :                             helperStyle: TextStyle(
    1156            1 :                               color: _getPasswordStrengthColor(),
    1157            3 :                               fontWeight: _accessPasswordController.text.isNotEmpty ? FontWeight.bold : FontWeight.normal,
    1158              :                             ),
    1159              :                             prefixIcon: const Icon(Icons.lock_outline),
    1160            1 :                             suffixIcon: Row(
    1161              :                               mainAxisSize: MainAxisSize.min,
    1162            1 :                               children: [
    1163              :                                 // 비밀번호 표시/숨김 토글 버튼
    1164            1 :                                 IconButton(
    1165            1 :                                   icon: Icon(
    1166            1 :                                     _obscurePassword ? Icons.visibility_off : Icons.visibility,
    1167            1 :                                     color: _obscurePassword ? Colors.grey : Colors.blue,
    1168              :                                   ),
    1169            0 :                                   onPressed: () {
    1170            0 :                                     setState(() {
    1171            0 :                                       _obscurePassword = !_obscurePassword;
    1172              :                                     });
    1173              :                                   },
    1174            1 :                                   tooltip: _obscurePassword ? '비밀번호 표시' : '비밀번호 숨김',
    1175              :                                 ),
    1176              :                                 // 도움말 버튼
    1177            1 :                                 IconButton(
    1178              :                                   icon: const Icon(Icons.help_outline),
    1179            0 :                                   onPressed: () {
    1180            0 :                                     ScaffoldMessenger.of(context).showSnackBar(
    1181              :                                       const SnackBar(
    1182              :                                         content: Text('비밀번호를 설정하면 공개 URL로 접근하는 사용자도 비밀번호를 입력해야 합니다'),
    1183              :                                         duration: Duration(seconds: 5),
    1184              :                                       ),
    1185              :                                     );
    1186              :                                   },
    1187              :                                   tooltip: '비밀번호 도움말',
    1188              :                                 ),
    1189              :                               ],
    1190              :                             ),
    1191              :                             // 비밀번호 강도에 따른 테두리 색상 변경
    1192            3 :                             enabledBorder: _accessPasswordController.text.isEmpty
    1193              :                                 ? null
    1194            0 :                                 : OutlineInputBorder(
    1195            0 :                                     borderSide: BorderSide(
    1196            0 :                                       color: _getPasswordStrengthColor(),
    1197              :                                       width: 1.5,
    1198              :                                     ),
    1199              :                                   ),
    1200              :                           ),
    1201            1 :                           obscureText: _obscurePassword,
    1202            0 :                           onChanged: (value) {
    1203              :                             // 비밀번호 입력 시 실시간으로 UI 업데이트
    1204            0 :                             setState(() {});
    1205              :                           },
    1206            1 :                           validator: (value) {
    1207            1 :                             if (value != null && value.isNotEmpty && value.length < 4) {
    1208              :                               return '비밀번호는 최소 4자 이상이어야 합니다';
    1209              :                             }
    1210              :                             return null;
    1211              :                           },
    1212              :                         ),
    1213            3 :                         if (_accessPasswordController.text.isNotEmpty)
    1214            0 :                           Padding(
    1215              :                             padding: const EdgeInsets.only(top: 8.0, left: 12.0),
    1216            0 :                             child: Row(
    1217            0 :                               children: [
    1218            0 :                                 Icon(Icons.lock, size: 16, color: _getPasswordStrengthColor()),
    1219              :                                 const SizedBox(width: 8),
    1220            0 :                                 Text(
    1221              :                                   '비밀번호 보호가 활성화되었습니다', 
    1222            0 :                                   style: TextStyle(
    1223            0 :                                     color: _getPasswordStrengthColor(), 
    1224              :                                     fontSize: 12, 
    1225              :                                     fontWeight: FontWeight.bold
    1226              :                                   ),
    1227              :                                 ),
    1228              :                               ],
    1229              :                             ),
    1230              :                           ),
    1231              :                       ],
    1232              :                     ),
    1233            1 :                     const SizedBox(height: 32),
    1234              :                     
    1235              :                     // 저장 버튼
    1236            1 :                     SizedBox(
    1237              :                       width: double.infinity,
    1238            1 :                       child: ElevatedButton(
    1239            1 :                         onPressed: _saveEvent,
    1240            1 :                         style: ElevatedButton.styleFrom(
    1241              :                           padding: const EdgeInsets.symmetric(vertical: 16),
    1242              :                         ),
    1243            1 :                         child: Text(
    1244            2 :                           widget.event == null ? '이벤트 생성' : '이벤트 수정',
    1245              :                           style: const TextStyle(fontSize: 16),
    1246              :                         ),
    1247              :                       ),
    1248              :                     ),
    1249              :                   ],
    1250              :                 ),
    1251              :               ),
    1252              :             ),
    1253              :     );
    1254              :     
    1255              :     // 테스트 환경에서 애니메이션 비활성화
    1256            1 :     if (TestUtils.isInTest) {
    1257            1 :       return TestUtils.disableAnimations(child: scaffold);
    1258              :     }
    1259              :     return scaffold;
    1260              :   }
    1261              : 
    1262              :   // 섹션 제목 위젯
    1263            1 :   Widget _buildSectionTitle(String title) {
    1264            1 :     return Column(
    1265              :       crossAxisAlignment: CrossAxisAlignment.start,
    1266            1 :       children: [
    1267            1 :         Text(
    1268              :           title,
    1269              :           style: const TextStyle(
    1270              :             fontSize: 18,
    1271              :             fontWeight: FontWeight.bold,
    1272              :           ),
    1273              :         ),
    1274              :         const Divider(),
    1275              :         const SizedBox(height: 8),
    1276              :       ],
    1277              :     );
    1278              :   }
    1279              :   
    1280              :   // 비밀번호 강도에 따른 색상 반환
    1281            1 :   Color _getPasswordStrengthColor() {
    1282            2 :     final password = _accessPasswordController.text;
    1283              :     
    1284            1 :     if (password.isEmpty) {
    1285            1 :       return Colors.grey[600]!;
    1286              :     }
    1287              :     
    1288            0 :     if (password.length < 4) {
    1289            0 :       return Colors.red[700]!;
    1290            0 :     } else if (password.length < 6) {
    1291            0 :       return Colors.orange[700]!;
    1292            0 :     } else if (password.length < 8) {
    1293            0 :       return Colors.yellow[700]!;
    1294              :     } else {
    1295              :       // 8자 이상이고 숫자와 특수문자를 포함하는지 확인
    1296            0 :       bool hasNumber = password.contains(RegExp(r'[0-9]'));
    1297            0 :       bool hasSpecialChar = password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'));
    1298              :       
    1299              :       if (hasNumber && hasSpecialChar) {
    1300            0 :         return Colors.green[700]!;
    1301              :       } else {
    1302            0 :         return Colors.blue[700]!;
    1303              :       }
    1304              :     }
    1305              :   }
    1306              :   
    1307              :   // 비밀번호 강도에 따른 도움말 텍스트 반환
    1308            1 :   String _getPasswordHelperText() {
    1309            2 :     final password = _accessPasswordController.text;
    1310              :     
    1311            1 :     if (password.isEmpty) {
    1312              :       return '설정 시 이벤트 접근에 비밀번호가 필요합니다';
    1313              :     }
    1314              :     
    1315            0 :     if (password.length < 4) {
    1316              :       return '비밀번호가 너무 짧습니다 (최소 4자)';
    1317            0 :     } else if (password.length < 6) {
    1318              :       return '비밀번호 강도: 약함';
    1319            0 :     } else if (password.length < 8) {
    1320              :       return '비밀번호 강도: 보통';
    1321              :     } else {
    1322              :       // 8자 이상이고 숫자와 특수문자를 포함하는지 확인
    1323            0 :       bool hasNumber = password.contains(RegExp(r'[0-9]'));
    1324            0 :       bool hasSpecialChar = password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'));
    1325              :       
    1326              :       if (hasNumber && hasSpecialChar) {
    1327              :         return '비밀번호 강도: 매우 강함';
    1328              :       } else {
    1329              :         return '비밀번호 강도: 강함';
    1330              :       }
    1331              :     }
    1332              :   }
    1333              :   
    1334              :   // 참가비 포맷팅 업데이트
    1335            1 :   void _updateFormattedFee(String value) {
    1336            1 :     if (value.isEmpty) {
    1337            0 :       _formattedFeeController.clear();
    1338              :       return;
    1339              :     }
    1340              :     
    1341            1 :     final numericValue = double.tryParse(value);
    1342              :     if (numericValue != null) {
    1343              :       // 현재 커서 위치 저장
    1344            3 :       final currentCursor = _formattedFeeController.selection.baseOffset;
    1345            2 :       final oldText = _formattedFeeController.text;
    1346            2 :       final newText = _currencyFormat.format(numericValue);
    1347              :       
    1348              :       // 커서 위치 계산 로직 개선
    1349              :       int newCursorPosition = currentCursor;
    1350            2 :       if (oldText.isNotEmpty && currentCursor > 0) {
    1351              :         // 이전 텍스트와 새 텍스트의 길이 차이 계산
    1352            3 :         final lengthDiff = newText.length - oldText.length;
    1353            3 :         newCursorPosition = (currentCursor + lengthDiff).clamp(0, newText.length);
    1354              :       } else {
    1355            0 :         newCursorPosition = newText.length;
    1356              :       }
    1357              :       
    1358            3 :       _formattedFeeController.value = TextEditingValue(
    1359              :         text: newText,
    1360            1 :         selection: TextSelection.collapsed(offset: newCursorPosition),
    1361              :       );
    1362              :     }
    1363              :   }
    1364              :   
    1365              :   // 참가비 변경 리스너
    1366            1 :   void _onParticipantFeeChanged() {
    1367            2 :     final value = _participantFeeController.text;
    1368            1 :     if (value.isEmpty) {
    1369            0 :       if (_formattedFeeController.text.isNotEmpty) {
    1370            0 :         setState(() {
    1371            0 :           _formattedFeeController.clear();
    1372              :         });
    1373              :       }
    1374              :     } else {
    1375              :       // 값이 있으면 포맷팅 업데이트
    1376            1 :       _updateFormattedFee(value);
    1377              :     }
    1378              :   }
    1379              :   
    1380              :   // 참가비 입력 상태에 따른 도움말 텍스트 반환
    1381            1 :   String _getParticipantFeeHelperText() {
    1382            3 :     if (_participantFeeController.text.isEmpty) {
    1383              :       return '통화 형식으로 자동 변환됩니다';
    1384              :     }
    1385              :     
    1386            3 :     final fee = double.tryParse(_participantFeeController.text);
    1387              :     if (fee == null) {
    1388              :       return '유효한 금액을 입력해주세요';
    1389              :     }
    1390              :     
    1391            1 :     if (fee <= 0) {
    1392              :       return '참가비는 0원 이상이어야 합니다';
    1393            1 :     } else if (fee > 1000000) {
    1394              :       return '최대 금액은 1,000,000원입니다';
    1395            1 :     } else if (fee >= 100000) {
    1396            0 :       return '참가비: ${_currencyFormat.format(fee)}';
    1397              :     } else {
    1398            3 :       return '참가비: ${_currencyFormat.format(fee)}';
    1399              :     }
    1400              :   }
    1401              :   
    1402              :   // 참가비 입력 상태에 따른 색상 반환
    1403            1 :   Color _getParticipantFeeColor() {
    1404            3 :     if (_participantFeeController.text.isEmpty) {
    1405            1 :       return Colors.grey[600]!;
    1406              :     }
    1407              :     
    1408            3 :     final fee = double.tryParse(_participantFeeController.text);
    1409              :     if (fee == null) {
    1410            0 :       return Colors.red[700]!;
    1411              :     }
    1412              :     
    1413            1 :     if (fee <= 0) {
    1414            0 :       return Colors.red[700]!;
    1415            1 :     } else if (fee > 1000000) {
    1416            0 :       return Colors.orange[700]!;
    1417              :     } else {
    1418            1 :       return Colors.green[700]!;
    1419              :     }
    1420              :   }
    1421              :   
    1422              :   // 참가비 입력 상태에 따른 아이콘 반환
    1423            1 :   IconData _getParticipantFeeIcon() {
    1424            3 :     if (_participantFeeController.text.isEmpty) {
    1425              :       return Icons.info_outline;
    1426              :     }
    1427              :     
    1428            3 :     final fee = double.tryParse(_participantFeeController.text);
    1429            1 :     if (fee == null || fee <= 0) {
    1430              :       return Icons.error_outline;
    1431            1 :     } else if (fee > 1000000) {
    1432              :       return Icons.warning;
    1433              :     } else {
    1434              :       return Icons.check_circle;
    1435              :     }
    1436              :   }
    1437              :   
    1438              :   // 참가비 입력 상태에 따른 상태 텍스트 반환
    1439            1 :   String _getParticipantFeeStatusText() {
    1440            3 :     if (_participantFeeController.text.isEmpty) {
    1441              :       return '참가비가 설정되지 않았습니다';
    1442              :     }
    1443              :     
    1444            3 :     final fee = double.tryParse(_participantFeeController.text);
    1445              :     if (fee == null) {
    1446              :       return '유효하지 않은 금액입니다';
    1447              :     }
    1448              :     
    1449            1 :     if (fee <= 0) {
    1450              :       return '참가비는 0원 이상이어야 합니다';
    1451            1 :     } else if (fee > 1000000) {
    1452              :       return '최대 금액을 초과했습니다';
    1453              :     } else {
    1454              :       return '참가비가 설정되었습니다';
    1455              :     }
    1456              :   }
    1457              : }
        

Generated by: LCOV version 2.3.1-1