feat(app): 예능 일정 상세 화면 추가
- ScheduleDetail 모델: broadcaster, replayUrl, varietyThumbnailUrl 필드 추가 - 예능 섹션: 썸네일(블러 배경 + contain) + 정보 카드 - 방송사 뱃지(카테고리 색상), 날짜/시간, 제목, 멤버, 다시보기 버튼 - 썸네일 없을 때 카테고리 색상 배경 + Tv 아이콘 표시 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aae606725f
commit
f73314d5dc
2 changed files with 174 additions and 0 deletions
|
|
@ -58,6 +58,10 @@ class ScheduleDetail {
|
||||||
// X 프로필
|
// X 프로필
|
||||||
final String? profileDisplayName;
|
final String? profileDisplayName;
|
||||||
final String? profileAvatarUrl;
|
final String? profileAvatarUrl;
|
||||||
|
// 예능 관련
|
||||||
|
final String? broadcaster;
|
||||||
|
final String? replayUrl;
|
||||||
|
final String? varietyThumbnailUrl;
|
||||||
|
|
||||||
ScheduleDetail({
|
ScheduleDetail({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
|
@ -80,6 +84,9 @@ class ScheduleDetail {
|
||||||
this.postUrl,
|
this.postUrl,
|
||||||
this.profileDisplayName,
|
this.profileDisplayName,
|
||||||
this.profileAvatarUrl,
|
this.profileAvatarUrl,
|
||||||
|
this.broadcaster,
|
||||||
|
this.replayUrl,
|
||||||
|
this.varietyThumbnailUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ScheduleDetail.fromJson(Map<String, dynamic> json) {
|
factory ScheduleDetail.fromJson(Map<String, dynamic> json) {
|
||||||
|
|
@ -110,6 +117,9 @@ class ScheduleDetail {
|
||||||
postUrl: json['postUrl'] as String?,
|
postUrl: json['postUrl'] as String?,
|
||||||
profileDisplayName: (json['profile'] as Map<String, dynamic>?)?['displayName'] as String?,
|
profileDisplayName: (json['profile'] as Map<String, dynamic>?)?['displayName'] as String?,
|
||||||
profileAvatarUrl: (json['profile'] as Map<String, dynamic>?)?['avatarUrl'] as String?,
|
profileAvatarUrl: (json['profile'] as Map<String, dynamic>?)?['avatarUrl'] as String?,
|
||||||
|
broadcaster: json['broadcaster'] as String?,
|
||||||
|
replayUrl: json['replayUrl'] as String?,
|
||||||
|
varietyThumbnailUrl: json['thumbnailUrl'] as String?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
/// 일정 상세 화면
|
/// 일정 상세 화면
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:ui';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
@ -21,6 +22,7 @@ class CategoryId {
|
||||||
static const int fansign = 5;
|
static const int fansign = 5;
|
||||||
static const int concert = 6;
|
static const int concert = 6;
|
||||||
static const int ticket = 7;
|
static const int ticket = 7;
|
||||||
|
static const int variety = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 일정 상세 Provider
|
/// 일정 상세 Provider
|
||||||
|
|
@ -190,6 +192,8 @@ class _ScheduleDetailViewState extends ConsumerState<ScheduleDetailView> {
|
||||||
return _buildYoutubeSection(schedule);
|
return _buildYoutubeSection(schedule);
|
||||||
case CategoryId.x:
|
case CategoryId.x:
|
||||||
return _buildXSection(schedule);
|
return _buildXSection(schedule);
|
||||||
|
case CategoryId.variety:
|
||||||
|
return _buildVarietySection(schedule);
|
||||||
default:
|
default:
|
||||||
return _buildDefaultSection(schedule);
|
return _buildDefaultSection(schedule);
|
||||||
}
|
}
|
||||||
|
|
@ -599,6 +603,166 @@ class _ScheduleDetailViewState extends ConsumerState<ScheduleDetailView> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 예능 섹션
|
||||||
|
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) {
|
Widget _buildDefaultSection(ScheduleDetail schedule) {
|
||||||
return _buildInfoCard(schedule);
|
return _buildInfoCard(schedule);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue