/// 일정 상세 화면 library; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../core/constants.dart'; import '../../models/schedule.dart'; import '../../services/schedules_service.dart'; import 'widgets/schedule_card.dart'; import 'widgets/member_chip.dart'; /// 카테고리 ID 상수 class CategoryId { static const int youtube = 2; static const int x = 3; static const int album = 4; static const int fansign = 5; static const int concert = 6; static const int ticket = 7; } /// 일정 상세 Provider final scheduleDetailProvider = FutureProvider.family((ref, id) async { return await getSchedule(id); }); class ScheduleDetailView extends ConsumerStatefulWidget { final int scheduleId; const ScheduleDetailView({super.key, required this.scheduleId}); @override ConsumerState createState() => _ScheduleDetailViewState(); } class _ScheduleDetailViewState extends ConsumerState { late int _currentScheduleId; @override void initState() { super.initState(); _currentScheduleId = widget.scheduleId; } /// 회차 변경 void _changeSchedule(int newId) { setState(() { _currentScheduleId = newId; }); } /// URL 열기 Future _launchUrl(String url) async { final uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } } /// 유튜브 비디오 ID 추출 String? _extractYoutubeVideoId(String? url) { if (url == null) return null; final shortMatch = RegExp(r'youtu\.be/([a-zA-Z0-9_-]{11})').firstMatch(url); if (shortMatch != null) return shortMatch.group(1); final watchMatch = RegExp(r'youtube\.com/watch\?v=([a-zA-Z0-9_-]{11})').firstMatch(url); if (watchMatch != null) return watchMatch.group(1); final shortsMatch = RegExp(r'youtube\.com/shorts/([a-zA-Z0-9_-]{11})').firstMatch(url); if (shortsMatch != null) return shortsMatch.group(1); return null; } /// X URL에서 username 추출 String? _extractXUsername(String? url) { if (url == null) return null; final match = RegExp(r'(?:twitter\.com|x\.com)/([^/]+)').firstMatch(url); return match?.group(1); } /// 날짜 포맷팅 (2026. 1. 15. (수)) String _formatFullDate(String dateStr) { final date = DateTime.parse(dateStr); final dayNames = ['일', '월', '화', '수', '목', '금', '토']; return '${date.year}. ${date.month}. ${date.day}. (${dayNames[date.weekday % 7]})'; } /// 단일 날짜 포맷팅 (1월 15일 (수) 19:00) String _formatSingleDate(String dateStr, String? timeStr) { final date = DateTime.parse(dateStr); final dayNames = ['일', '월', '화', '수', '목', '금', '토']; var result = '${date.month}월 ${date.day}일 (${dayNames[date.weekday % 7]})'; if (timeStr != null && timeStr.length >= 5) { result += ' ${timeStr.substring(0, 5)}'; } return result; } /// X용 날짜/시간 포맷팅 (오후 2:30 · 2026년 1월 15일) String _formatXDateTime(String dateStr, String? timeStr) { final date = DateTime.parse(dateStr); final year = date.year; final month = date.month; final day = date.day; var result = '$year년 $month월 $day일'; if (timeStr != null && timeStr.length >= 5) { final parts = timeStr.split(':'); final hours = int.parse(parts[0]); final minutes = parts[1]; final period = hours < 12 ? '오전' : '오후'; final hour12 = hours == 0 ? 12 : (hours > 12 ? hours - 12 : hours); result = '$period $hour12:$minutes · $result'; } return result; } @override Widget build(BuildContext context) { final scheduleAsync = ref.watch(scheduleDetailProvider(_currentScheduleId)); return Scaffold( backgroundColor: AppColors.background, appBar: AppBar( backgroundColor: Colors.white, elevation: 0, scrolledUnderElevation: 0.5, leading: IconButton( icon: const Icon(LucideIcons.chevronLeft, size: 24), onPressed: () => Navigator.of(context).pop(), ), title: scheduleAsync.whenOrNull( data: (schedule) => Text( schedule.categoryName ?? '', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: parseColor(schedule.categoryColor), ), ), ), centerTitle: true, ), body: scheduleAsync.when( loading: () => const Center( child: CircularProgressIndicator(color: AppColors.primary), ), error: (error, stack) => _buildErrorView(), data: (schedule) => _buildContent(schedule), ), ); } /// 에러 화면 Widget _buildErrorView() { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 80, height: 80, decoration: BoxDecoration( color: AppColors.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(20), ), child: Icon( LucideIcons.calendar, size: 40, color: AppColors.primary.withValues(alpha: 0.4), ), ), const SizedBox(height: 24), const Text( '일정을 찾을 수 없습니다', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), const SizedBox(height: 8), const Text( '요청하신 일정이 존재하지 않거나\n삭제되었을 수 있습니다.', textAlign: TextAlign.center, style: TextStyle( fontSize: 14, color: AppColors.textSecondary, ), ), const SizedBox(height: 32), OutlinedButton.icon( onPressed: () => Navigator.of(context).pop(), icon: const Icon(LucideIcons.arrowLeft, size: 18), label: const Text('돌아가기'), style: OutlinedButton.styleFrom( foregroundColor: AppColors.primary, side: const BorderSide(color: AppColors.primary), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), ), ], ), ), ); } /// 메인 컨텐츠 Widget _buildContent(ScheduleDetail schedule) { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: _buildCategorySection(schedule), ); } /// 카테고리별 섹션 Widget _buildCategorySection(ScheduleDetail schedule) { switch (schedule.categoryId) { case CategoryId.youtube: return _buildYoutubeSection(schedule); case CategoryId.x: return _buildXSection(schedule); case CategoryId.concert: return _buildConcertSection(schedule); default: return _buildDefaultSection(schedule); } } /// 유튜브 섹션 Widget _buildYoutubeSection(ScheduleDetail schedule) { final videoId = _extractYoutubeVideoId(schedule.sourceUrl); final isShorts = schedule.sourceUrl?.contains('/shorts/') ?? false; if (videoId == null) return _buildDefaultSection(schedule); // 썸네일 URL final thumbnailUrl = 'https://img.youtube.com/vi/$videoId/maxresdefault.jpg'; return Column( children: [ // 썸네일 + 재생 버튼 (클릭시 YouTube로 이동) GestureDetector( onTap: () => _launchUrl(schedule.sourceUrl!), child: isShorts ? Center( child: SizedBox( width: 200, child: ClipRRect( borderRadius: BorderRadius.circular(12), child: AspectRatio( aspectRatio: 9 / 16, child: Stack( alignment: Alignment.center, children: [ CachedNetworkImage( imageUrl: thumbnailUrl, fit: BoxFit.cover, width: double.infinity, height: double.infinity, placeholder: (_, _) => Container( color: Colors.grey[900], ), errorWidget: (_, _, _) => Container( color: Colors.grey[900], child: const Icon(LucideIcons.play, size: 48, color: Colors.white54), ), ), _buildYoutubePlayButton(), ], ), ), ), ), ) : ClipRRect( borderRadius: BorderRadius.circular(12), child: AspectRatio( aspectRatio: 16 / 9, child: Stack( alignment: Alignment.center, children: [ CachedNetworkImage( imageUrl: thumbnailUrl, fit: BoxFit.cover, width: double.infinity, height: double.infinity, placeholder: (_, _) => Container( color: Colors.grey[900], ), errorWidget: (_, _, _) => Container( color: Colors.grey[900], child: const Icon(LucideIcons.play, size: 48, color: Colors.white54), ), ), _buildYoutubePlayButton(), ], ), ), ), ), const SizedBox(height: 16), // 정보 카드 (버튼 없음) _buildInfoCard(schedule, bottomButton: null), ], ); } /// 유튜브 재생 버튼 (실제 유튜브 아이콘) Widget _buildYoutubePlayButton() { return Container( width: 68, height: 48, decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.4), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Center( child: CustomPaint( size: const Size(24, 24), painter: _YoutubePlayIconPainter(), ), ), ); } /// X 섹션 (웹과 동일) Widget _buildXSection(ScheduleDetail schedule) { final username = _extractXUsername(schedule.sourceUrl); final displayName = schedule.sourceName ?? username ?? 'Unknown'; return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 헤더 Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), child: Row( children: [ // 프로필 이미지 Container( width: 40, height: 40, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.grey[700]!, Colors.grey[900]!], ), shape: BoxShape.circle, ), child: Center( child: Text( displayName[0].toUpperCase(), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16, ), ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Flexible( child: Text( displayName, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 4), // 인증 배지 (웹과 동일한 SVG 형태) _buildVerifiedBadge(), ], ), if (username != null) Text( '@$username', style: TextStyle( fontSize: 13, color: Colors.grey[500], ), ), ], ), ), ], ), ), // 본문 Padding( padding: const EdgeInsets.all(16), child: Text( decodeHtmlEntities(schedule.description ?? schedule.title), style: const TextStyle( fontSize: 15, height: 1.5, color: AppColors.textPrimary, ), ), ), // 이미지 if (schedule.imageUrl != null) ...[ Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: CachedNetworkImage( imageUrl: schedule.imageUrl!, fit: BoxFit.cover, placeholder: (_, _) => Container( height: 200, color: Colors.grey[200], ), errorWidget: (_, _, _) => const SizedBox.shrink(), ), ), ), ], // 날짜/시간 Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( border: Border(top: BorderSide(color: Colors.grey[100]!)), ), child: Text( _formatXDateTime(schedule.date, schedule.time), style: TextStyle( fontSize: 14, color: Colors.grey[500], ), ), ), // X에서 보기 버튼 Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey[50], border: Border(top: BorderSide(color: Colors.grey[100]!)), ), child: SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => _launchUrl(schedule.sourceUrl ?? ''), style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[900], foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // X 로고 _buildXLogo(14), const SizedBox(width: 8), const Text( 'X에서 보기', style: TextStyle(fontWeight: FontWeight.w500), ), ], ), ), ), ), ], ), ); } /// 인증 배지 (웹과 동일한 SVG) Widget _buildVerifiedBadge() { return SizedBox( width: 16, height: 16, child: CustomPaint( painter: _VerifiedBadgePainter(), ), ); } /// X 로고 Widget _buildXLogo(double size) { return SizedBox( width: size, height: size, child: CustomPaint( painter: _XLogoPainter(), ), ); } /// 콘서트 섹션 Widget _buildConcertSection(ScheduleDetail schedule) { final hasLocation = schedule.locationLat != null && schedule.locationLng != null; final hasPoster = schedule.images.isNotEmpty; final hasMultipleDates = schedule.relatedDates.length > 1; return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 12, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 헤더 (포스터 여백 제거) Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [AppColors.primary, AppColors.primary.withValues(alpha: 0.8)], ), borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), ), child: Row( children: [ if (hasPoster) ClipRRect( borderRadius: const BorderRadius.only(topLeft: Radius.circular(12)), child: CachedNetworkImage( imageUrl: schedule.images[0], width: 56, height: 72, fit: BoxFit.cover, placeholder: (_, _) => Container( width: 56, height: 72, color: Colors.white24, ), errorWidget: (_, _, _) => const SizedBox.shrink(), ), ), Expanded( child: Padding( padding: const EdgeInsets.all(16), child: Text( decodeHtmlEntities(schedule.title), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 15, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ), ], ), ), // 정보 목록 Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 공연 일정 _buildSectionLabel(LucideIcons.calendar, '공연 일정'), const SizedBox(height: 8), if (hasMultipleDates) ...schedule.relatedDates.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; final isCurrent = item.id == schedule.id; return Padding( padding: const EdgeInsets.only(bottom: 8), child: InkWell( onTap: isCurrent ? null : () => _changeSchedule(item.id), borderRadius: BorderRadius.circular(8), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10, ), decoration: BoxDecoration( color: isCurrent ? AppColors.primary : Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Text( '${index + 1}회차 ${_formatSingleDate(item.date, item.time)}', style: TextStyle( fontSize: 13, // 볼드체 제거 color: isCurrent ? Colors.white : AppColors.textPrimary, ), ), ), ), ); }) else Text( _formatSingleDate(schedule.date, schedule.time), style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), const SizedBox(height: 16), // 장소 if (schedule.locationName != null) ...[ _buildSectionLabel(LucideIcons.mapPin, '장소'), const SizedBox(height: 8), Text( schedule.locationName!, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 14, ), ), if (schedule.locationAddress != null) ...[ const SizedBox(height: 2), Text( schedule.locationAddress!, style: const TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), ], const SizedBox(height: 16), ], // 위치 (지도) if (hasLocation) ...[ _buildSectionLabel(LucideIcons.navigation, '위치'), const SizedBox(height: 8), // 지도 플레이스홀더 (탭시 카카오맵으로 이동) GestureDetector( onTap: () => _launchUrl( 'https://map.kakao.com/link/map/${Uri.encodeComponent(schedule.locationName!)},${schedule.locationLat},${schedule.locationLng}', ), child: Container( height: 160, decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.border), ), child: Stack( children: [ // 지도 이미지 (카카오 Static Map API) ClipRRect( borderRadius: BorderRadius.circular(11), child: CachedNetworkImage( imageUrl: 'https://map.kakao.com/link/map/${Uri.encodeComponent(schedule.locationName!)},${schedule.locationLat},${schedule.locationLng}', fit: BoxFit.cover, width: double.infinity, height: double.infinity, placeholder: (_, _) => Container( color: Colors.grey[200], child: const Center( child: Icon(LucideIcons.map, size: 32, color: Colors.grey), ), ), errorWidget: (_, _, _) => Container( color: Colors.grey[200], child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(LucideIcons.navigation, size: 28, color: Colors.grey[400]), const SizedBox(height: 8), Text( schedule.locationName!, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Colors.grey[600], ), textAlign: TextAlign.center, ), const SizedBox(height: 4), Text( '탭하여 지도에서 보기', style: TextStyle( fontSize: 11, color: Colors.grey[400], ), ), ], ), ), ), ), ), ], ), ), ), const SizedBox(height: 16), ], // 설명 if (schedule.description != null && schedule.description!.isNotEmpty) ...[ Container( width: double.infinity, padding: const EdgeInsets.only(top: 16), decoration: BoxDecoration( border: Border( top: BorderSide(color: AppColors.divider), ), ), child: Text( decodeHtmlEntities(schedule.description!), style: const TextStyle( fontSize: 14, color: AppColors.textSecondary, height: 1.5, ), ), ), ], ], ), ), // 버튼 영역 Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: Column( children: [ // 길찾기 버튼 (파란색) if (hasLocation) SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => _launchUrl( 'https://map.kakao.com/link/to/${Uri.encodeComponent(schedule.locationName!)},${schedule.locationLat},${schedule.locationLng}'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF3B82F6), // blue-500 foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: const [ Icon(LucideIcons.navigation, size: 18), SizedBox(width: 8), Text('길찾기'), ], ), ), ), if (hasLocation && schedule.sourceUrl != null) const SizedBox(height: 8), // 상세 정보 버튼 if (schedule.sourceUrl != null) SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => _launchUrl(schedule.sourceUrl!), style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[900], foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: const [ Icon(LucideIcons.externalLink, size: 18), SizedBox(width: 8), Text('상세 정보 보기'), ], ), ), ), ], ), ), ], ), ); } /// 기본 섹션 Widget _buildDefaultSection(ScheduleDetail schedule) { return _buildInfoCard( schedule, bottomButton: schedule.sourceUrl != null ? SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => _launchUrl(schedule.sourceUrl!), style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[900], foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: const [ Icon(LucideIcons.externalLink, size: 18), SizedBox(width: 8), Text('원본 보기'), ], ), ), ) : null, ); } /// 정보 카드 Widget _buildInfoCard(ScheduleDetail schedule, {Widget? bottomButton}) { final isFullGroup = schedule.members.length >= 5; return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 12, offset: const Offset(0, 2), ), ], ), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 제목 Text( decodeHtmlEntities(schedule.title), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: AppColors.textPrimary, height: 1.4, ), ), const SizedBox(height: 12), // 메타 정보 Wrap( spacing: 12, runSpacing: 8, children: [ _buildMetaItem(LucideIcons.calendar, _formatFullDate(schedule.date)), if (schedule.formattedTime != null) _buildMetaItem(LucideIcons.clock, schedule.formattedTime!), if (schedule.sourceName != null) _buildMetaItem(LucideIcons.link2, schedule.sourceName!), ], ), // 멤버 if (schedule.members.isNotEmpty) ...[ const SizedBox(height: 16), Container( width: double.infinity, height: 1, color: AppColors.divider, ), const SizedBox(height: 16), Wrap( spacing: 6, runSpacing: 6, children: isFullGroup ? [const MemberChip(name: '프로미스나인')] : schedule.members .map((m) => MemberChip(name: m.name)) .toList(), ), ], // 설명 if (schedule.description != null && schedule.description!.isNotEmpty) ...[ const SizedBox(height: 16), Text( decodeHtmlEntities(schedule.description!), style: const TextStyle( fontSize: 14, color: AppColors.textSecondary, height: 1.5, ), ), ], // 하단 버튼 if (bottomButton != null) ...[ const SizedBox(height: 16), bottomButton, ], ], ), ); } /// 메타 아이템 Widget _buildMetaItem(IconData icon, String text) { return Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: AppColors.textSecondary), const SizedBox(width: 4), Text( text, style: const TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), ], ); } /// 섹션 라벨 Widget _buildSectionLabel(IconData icon, String text) { return Row( children: [ Icon(icon, size: 14, color: AppColors.textSecondary), const SizedBox(width: 6), Text( text, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppColors.textSecondary, ), ), ], ); } } /// 유튜브 재생 아이콘 페인터 class _YoutubePlayIconPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.white ..style = PaintingStyle.fill; final path = Path() ..moveTo(size.width * 0.35, size.height * 0.25) ..lineTo(size.width * 0.35, size.height * 0.75) ..lineTo(size.width * 0.75, size.height * 0.5) ..close(); canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } /// 인증 배지 페인터 (웹과 동일한 디자인) class _VerifiedBadgePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = const Color(0xFF3B82F6) // blue-500 ..style = PaintingStyle.fill; // 배지 배경 (둥근 별 모양) final center = Offset(size.width / 2, size.height / 2); final radius = size.width / 2; canvas.drawCircle(center, radius, paint); // 체크마크 final checkPaint = Paint() ..color = Colors.white ..style = PaintingStyle.stroke ..strokeWidth = 1.5 ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round; final checkPath = Path() ..moveTo(size.width * 0.28, size.height * 0.52) ..lineTo(size.width * 0.45, size.height * 0.68) ..lineTo(size.width * 0.72, size.height * 0.35); canvas.drawPath(checkPath, checkPaint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } /// X 로고 페인터 class _XLogoPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.white ..style = PaintingStyle.fill; final path = Path(); // X 로고 경로 (간소화) path.moveTo(size.width * 0.76, size.height * 0.09); path.lineTo(size.width * 0.9, size.height * 0.09); path.lineTo(size.width * 0.6, size.height * 0.44); path.lineTo(size.width * 0.95, size.height * 0.91); path.lineTo(size.width * 0.67, size.height * 0.91); path.lineTo(size.width * 0.46, size.height * 0.63); path.lineTo(size.width * 0.21, size.height * 0.91); path.lineTo(size.width * 0.07, size.height * 0.91); path.lineTo(size.width * 0.4, size.height * 0.53); path.lineTo(size.width * 0.05, size.height * 0.09); path.lineTo(size.width * 0.34, size.height * 0.09); path.lineTo(size.width * 0.52, size.height * 0.35); path.close(); canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }