diff --git a/app/lib/models/schedule.dart b/app/lib/models/schedule.dart index fbe70a5..91f68d1 100644 --- a/app/lib/models/schedule.dart +++ b/app/lib/models/schedule.dart @@ -58,6 +58,10 @@ class ScheduleDetail { // X 프로필 final String? profileDisplayName; final String? profileAvatarUrl; + // 예능 관련 + final String? broadcaster; + final String? replayUrl; + final String? varietyThumbnailUrl; ScheduleDetail({ required this.id, @@ -80,6 +84,9 @@ class ScheduleDetail { this.postUrl, this.profileDisplayName, this.profileAvatarUrl, + this.broadcaster, + this.replayUrl, + this.varietyThumbnailUrl, }); factory ScheduleDetail.fromJson(Map json) { @@ -110,6 +117,9 @@ class ScheduleDetail { postUrl: json['postUrl'] as String?, profileDisplayName: (json['profile'] as Map?)?['displayName'] as String?, profileAvatarUrl: (json['profile'] as Map?)?['avatarUrl'] as String?, + broadcaster: json['broadcaster'] as String?, + replayUrl: json['replayUrl'] as String?, + varietyThumbnailUrl: json['thumbnailUrl'] as String?, ); } diff --git a/app/lib/views/schedule/schedule_detail_view.dart b/app/lib/views/schedule/schedule_detail_view.dart index 925bc0d..b154cea 100644 --- a/app/lib/views/schedule/schedule_detail_view.dart +++ b/app/lib/views/schedule/schedule_detail_view.dart @@ -1,6 +1,7 @@ /// 일정 상세 화면 library; +import 'dart:ui'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -21,6 +22,7 @@ class CategoryId { static const int fansign = 5; static const int concert = 6; static const int ticket = 7; + static const int variety = 10; } /// 일정 상세 Provider @@ -190,6 +192,8 @@ class _ScheduleDetailViewState extends ConsumerState { return _buildYoutubeSection(schedule); case CategoryId.x: return _buildXSection(schedule); + case CategoryId.variety: + return _buildVarietySection(schedule); default: return _buildDefaultSection(schedule); } @@ -599,6 +603,166 @@ class _ScheduleDetailViewState extends ConsumerState { ); } + /// 예능 섹션 + Widget _buildVarietySection(ScheduleDetail schedule) { + final members = schedule.members; + final isFullGroup = members.length >= 5; + final hasThumbnail = schedule.varietyThumbnailUrl != null; + final categoryColor = parseColor(schedule.categoryColor); + + return Column( + children: [ + // 썸네일 + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: SizedBox( + width: double.infinity, + height: 200, + child: hasThumbnail + ? Stack( + fit: StackFit.expand, + children: [ + // 블러 배경 + CachedNetworkImage( + imageUrl: schedule.varietyThumbnailUrl!, + fit: BoxFit.cover, + color: Colors.black.withValues(alpha: 0.3), + colorBlendMode: BlendMode.darken, + ), + ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), + child: Container(color: Colors.transparent), + ), + ), + // 메인 이미지 + CachedNetworkImage( + imageUrl: schedule.varietyThumbnailUrl!, + fit: BoxFit.contain, + placeholder: (_, _) => Container(color: Colors.grey[200]), + ), + ], + ) + : Container( + color: categoryColor.withValues(alpha: 0.1), + child: Center( + child: Icon(LucideIcons.tv, size: 40, color: categoryColor), + ), + ), + ), + ), + const SizedBox(height: 12), + // 정보 카드 + Container( + width: double.infinity, + 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: [ + // 방송사 + 날짜 + Row( + children: [ + if (schedule.broadcaster != null) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: categoryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(LucideIcons.tv, size: 11, color: categoryColor), + const SizedBox(width: 4), + Text( + schedule.broadcaster!, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: categoryColor, + ), + ), + ], + ), + ), + Text( + _formatFullDate(schedule.date), + style: TextStyle(fontSize: 12, color: Colors.grey[400]), + ), + if (schedule.formattedTime != null) ...[ + Text( + ' · ${schedule.formattedTime}', + style: TextStyle(fontSize: 12, color: Colors.grey[400]), + ), + ], + ], + ), + const SizedBox(height: 10), + // 제목 + Text( + decodeHtmlEntities(schedule.title), + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + height: 1.4, + ), + ), + // 멤버 + if (members.isNotEmpty) ...[ + const SizedBox(height: 12), + Wrap( + spacing: 6, + runSpacing: 6, + children: isFullGroup + ? [const MemberChip(name: '프로미스나인')] + : members.map((m) => MemberChip(name: m.name)).toList(), + ), + ], + // 다시보기 버튼 + if (schedule.replayUrl != null) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.only(top: 16), + decoration: const BoxDecoration( + border: Border(top: BorderSide(color: AppColors.divider)), + ), + child: SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () => _launchUrl(schedule.replayUrl!), + icon: const Icon(LucideIcons.externalLink, size: 16), + label: const Text('다시보기', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[900], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + elevation: 0, + ), + ), + ), + ), + ], + ], + ), + ), + ], + ); + } + /// 기본 섹션 Widget _buildDefaultSection(ScheduleDetail schedule) { return _buildInfoCard(schedule);