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