refactor(schedule): 위젯 파일 분리
- schedule_view.dart에서 위젯들을 별도 파일로 분리 - widgets/member_chip.dart: MemberChip, SearchMemberChip - widgets/schedule_card.dart: ScheduleCard, AnimatedScheduleCard - widgets/search_card.dart: SearchScheduleCard - 공용 유틸 함수 분리 (decodeHtmlEntities, parseColor) - 파일 크기 2001줄 → 1440줄 (28% 감소) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
fbe18b6157
commit
88f15a3ec1
4 changed files with 594 additions and 569 deletions
|
|
@ -11,17 +11,8 @@ import 'package:expandable_page_view/expandable_page_view.dart';
|
||||||
import '../../core/constants.dart';
|
import '../../core/constants.dart';
|
||||||
import '../../models/schedule.dart';
|
import '../../models/schedule.dart';
|
||||||
import '../../controllers/schedule_controller.dart';
|
import '../../controllers/schedule_controller.dart';
|
||||||
|
import 'widgets/schedule_card.dart';
|
||||||
/// HTML 엔티티 디코딩
|
import 'widgets/search_card.dart';
|
||||||
String decodeHtmlEntities(String text) {
|
|
||||||
return text
|
|
||||||
.replaceAll('&', '&')
|
|
||||||
.replaceAll('<', '<')
|
|
||||||
.replaceAll('>', '>')
|
|
||||||
.replaceAll('"', '"')
|
|
||||||
.replaceAll(''', "'")
|
|
||||||
.replaceAll(' ', ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
class ScheduleView extends ConsumerStatefulWidget {
|
class ScheduleView extends ConsumerStatefulWidget {
|
||||||
const ScheduleView({super.key});
|
const ScheduleView({super.key});
|
||||||
|
|
@ -255,17 +246,6 @@ class _ScheduleViewState extends ConsumerState<ScheduleView>
|
||||||
return days[date.weekday % 7];
|
return days[date.weekday % 7];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 카테고리 색상 파싱
|
|
||||||
Color _parseColor(String? colorStr) {
|
|
||||||
if (colorStr == null || colorStr.isEmpty) return AppColors.textTertiary;
|
|
||||||
try {
|
|
||||||
final hex = colorStr.replaceFirst('#', '');
|
|
||||||
return Color(int.parse('FF$hex', radix: 16));
|
|
||||||
} catch (_) {
|
|
||||||
return AppColors.textTertiary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final scheduleState = ref.watch(scheduleProvider);
|
final scheduleState = ref.watch(scheduleProvider);
|
||||||
|
|
@ -687,9 +667,9 @@ class _ScheduleViewState extends ConsumerState<ScheduleView>
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
bottom: index < searchState.results.length - 1 ? 12 : 0,
|
bottom: index < searchState.results.length - 1 ? 12 : 0,
|
||||||
),
|
),
|
||||||
child: _SearchScheduleCard(
|
child: SearchScheduleCard(
|
||||||
schedule: schedule,
|
schedule: schedule,
|
||||||
categoryColor: _parseColor(schedule.categoryColor),
|
categoryColor: parseColor(schedule.categoryColor),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1314,7 +1294,7 @@ class _ScheduleViewState extends ConsumerState<ScheduleView>
|
||||||
height: 4,
|
height: 4,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _parseColor(schedule.categoryColor),
|
color: parseColor(schedule.categoryColor),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -1401,7 +1381,7 @@ class _ScheduleViewState extends ConsumerState<ScheduleView>
|
||||||
margin:
|
margin:
|
||||||
const EdgeInsets.symmetric(horizontal: 1),
|
const EdgeInsets.symmetric(horizontal: 1),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _parseColor(schedule.categoryColor),
|
color: parseColor(schedule.categoryColor),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -1447,555 +1427,14 @@ class _ScheduleViewState extends ConsumerState<ScheduleView>
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
bottom: index < state.selectedDateSchedules.length - 1 ? 12 : 0),
|
bottom: index < state.selectedDateSchedules.length - 1 ? 12 : 0),
|
||||||
child: _AnimatedScheduleCard(
|
child: AnimatedScheduleCard(
|
||||||
key: ValueKey('${schedule.id}_${state.selectedDate.toString()}'),
|
key: ValueKey('${schedule.id}_${state.selectedDate.toString()}'),
|
||||||
index: index,
|
index: index,
|
||||||
schedule: schedule,
|
schedule: schedule,
|
||||||
categoryColor: _parseColor(schedule.categoryColor),
|
categoryColor: parseColor(schedule.categoryColor),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 애니메이션이 적용된 일정 카드 래퍼
|
|
||||||
class _AnimatedScheduleCard extends StatefulWidget {
|
|
||||||
final int index;
|
|
||||||
final Schedule schedule;
|
|
||||||
final Color categoryColor;
|
|
||||||
|
|
||||||
const _AnimatedScheduleCard({
|
|
||||||
super.key,
|
|
||||||
required this.index,
|
|
||||||
required this.schedule,
|
|
||||||
required this.categoryColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_AnimatedScheduleCard> createState() => _AnimatedScheduleCardState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AnimatedScheduleCardState extends State<_AnimatedScheduleCard>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late AnimationController _controller;
|
|
||||||
late Animation<double> _fadeAnimation;
|
|
||||||
late Animation<double> _slideAnimation;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
|
|
||||||
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
|
||||||
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 웹과 동일: x: -10px 에서 0으로 (spring 효과)
|
|
||||||
_slideAnimation = Tween<double>(begin: -10.0, end: 0.0).animate(
|
|
||||||
CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 순차적 애니메이션 (index * 30ms 딜레이) - 더 빠르게
|
|
||||||
Future.delayed(Duration(milliseconds: widget.index * 30), () {
|
|
||||||
if (mounted) _controller.forward();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: _controller,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Opacity(
|
|
||||||
opacity: _fadeAnimation.value,
|
|
||||||
child: Transform.translate(
|
|
||||||
offset: Offset(_slideAnimation.value, 0),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: _ScheduleCard(
|
|
||||||
schedule: widget.schedule,
|
|
||||||
categoryColor: widget.categoryColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 일정 카드 위젯
|
|
||||||
class _ScheduleCard extends StatelessWidget {
|
|
||||||
final Schedule schedule;
|
|
||||||
final Color categoryColor;
|
|
||||||
|
|
||||||
const _ScheduleCard({
|
|
||||||
required this.schedule,
|
|
||||||
required this.categoryColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final memberList = schedule.memberList;
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withValues(alpha: 0.04),
|
|
||||||
blurRadius: 12,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
border: Border.all(
|
|
||||||
color: AppColors.border.withValues(alpha: 0.5),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// 시간 및 카테고리 뱃지
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
// 시간 뱃지
|
|
||||||
if (schedule.formattedTime != null)
|
|
||||||
Container(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: categoryColor,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.access_time,
|
|
||||||
size: 10,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
schedule.formattedTime!,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (schedule.formattedTime != null) const SizedBox(width: 6),
|
|
||||||
// 카테고리 뱃지
|
|
||||||
if (schedule.categoryName != null)
|
|
||||||
Container(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: categoryColor.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
schedule.categoryName!,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: categoryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
// 제목
|
|
||||||
Text(
|
|
||||||
decodeHtmlEntities(schedule.title),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColors.textPrimary,
|
|
||||||
height: 1.4,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 출처
|
|
||||||
if (schedule.sourceName != null) ...[
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.link,
|
|
||||||
size: 11,
|
|
||||||
color: AppColors.textTertiary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
schedule.sourceName!,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: AppColors.textTertiary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
// 멤버
|
|
||||||
if (memberList.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.only(top: 12),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
top: BorderSide(color: AppColors.divider, width: 1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 6,
|
|
||||||
runSpacing: 6,
|
|
||||||
children: memberList.length >= 5
|
|
||||||
? [
|
|
||||||
_MemberChip(name: '프로미스나인'),
|
|
||||||
]
|
|
||||||
: memberList
|
|
||||||
.map((name) => _MemberChip(name: name))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 멤버 칩 위젯
|
|
||||||
class _MemberChip extends StatelessWidget {
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
const _MemberChip({required this.name});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: const LinearGradient(
|
|
||||||
colors: [AppColors.primary, AppColors.primaryDark],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: AppColors.primary.withValues(alpha: 0.3),
|
|
||||||
blurRadius: 4,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
name,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 검색 결과 카드 (웹과 동일한 디자인 - 왼쪽에 날짜, 오른쪽에 내용)
|
|
||||||
class _SearchScheduleCard extends StatelessWidget {
|
|
||||||
final Schedule schedule;
|
|
||||||
final Color categoryColor;
|
|
||||||
|
|
||||||
const _SearchScheduleCard({
|
|
||||||
required this.schedule,
|
|
||||||
required this.categoryColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 날짜 파싱
|
|
||||||
Map<String, dynamic>? _parseDate(String? dateStr) {
|
|
||||||
if (dateStr == null) return null;
|
|
||||||
try {
|
|
||||||
final date = DateTime.parse(dateStr);
|
|
||||||
const weekdays = ['일', '월', '화', '수', '목', '금', '토'];
|
|
||||||
return {
|
|
||||||
'year': date.year,
|
|
||||||
'month': date.month,
|
|
||||||
'day': date.day,
|
|
||||||
'weekday': weekdays[date.weekday % 7],
|
|
||||||
'isSunday': date.weekday == 7,
|
|
||||||
'isSaturday': date.weekday == 6,
|
|
||||||
};
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final memberList = schedule.memberList;
|
|
||||||
final dateInfo = _parseDate(schedule.date);
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withValues(alpha: 0.04),
|
|
||||||
blurRadius: 12,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
border: Border.all(
|
|
||||||
color: AppColors.border.withValues(alpha: 0.5),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: IntrinsicHeight(
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
// 왼쪽 날짜 영역 (카드 높이에 맞춤)
|
|
||||||
if (dateInfo != null)
|
|
||||||
Container(
|
|
||||||
width: 72,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 6),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: AppColors.background,
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(7),
|
|
||||||
bottomLeft: Radius.circular(7),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
// 년도
|
|
||||||
Text(
|
|
||||||
'${dateInfo['year']}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontFamily: 'Pretendard',
|
|
||||||
fontSize: 10,
|
|
||||||
color: AppColors.textTertiary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 월.일 (줄바꿈 방지)
|
|
||||||
FittedBox(
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
child: Text(
|
|
||||||
'${dateInfo['month']}.${dateInfo['day']}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontFamily: 'Pretendard',
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 요일
|
|
||||||
Text(
|
|
||||||
'${dateInfo['weekday']}요일',
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'Pretendard',
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: dateInfo['isSunday'] == true
|
|
||||||
? Colors.red.shade500
|
|
||||||
: dateInfo['isSaturday'] == true
|
|
||||||
? Colors.blue.shade500
|
|
||||||
: AppColors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 오른쪽 콘텐츠 영역
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// 시간 및 카테고리 뱃지
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
// 시간 뱃지
|
|
||||||
if (schedule.formattedTime != null)
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: categoryColor,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.access_time,
|
|
||||||
size: 10,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
schedule.formattedTime!,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontFamily: 'Pretendard',
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (schedule.formattedTime != null)
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
// 카테고리 뱃지
|
|
||||||
if (schedule.categoryName != null)
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: categoryColor.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
schedule.categoryName!,
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'Pretendard',
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: categoryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
// 제목
|
|
||||||
Text(
|
|
||||||
decodeHtmlEntities(schedule.title),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontFamily: 'Pretendard',
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColors.textPrimary,
|
|
||||||
height: 1.4,
|
|
||||||
),
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
// 출처 (빈 문자열이 아닌 경우에만 표시)
|
|
||||||
if (schedule.sourceName != null && schedule.sourceName!.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.link,
|
|
||||||
size: 12,
|
|
||||||
color: AppColors.textTertiary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
schedule.sourceName!,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontFamily: 'Pretendard',
|
|
||||||
fontSize: 12,
|
|
||||||
color: AppColors.textTertiary,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
// 멤버
|
|
||||||
if (memberList.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
// divider (전체 너비)
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 1,
|
|
||||||
color: AppColors.divider,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Wrap(
|
|
||||||
spacing: 4,
|
|
||||||
runSpacing: 4,
|
|
||||||
children: memberList.length >= 5
|
|
||||||
? [
|
|
||||||
_SearchMemberChip(name: '프로미스나인'),
|
|
||||||
]
|
|
||||||
: memberList
|
|
||||||
.map((name) => _SearchMemberChip(name: name))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 검색 결과용 멤버 칩 (작은 사이즈)
|
|
||||||
class _SearchMemberChip extends StatelessWidget {
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
const _SearchMemberChip({required this.name});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: const LinearGradient(
|
|
||||||
colors: [AppColors.primary, AppColors.primaryDark],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: AppColors.primary.withValues(alpha: 0.3),
|
|
||||||
blurRadius: 3,
|
|
||||||
offset: const Offset(0, 1),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
name,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontFamily: 'Pretendard',
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
76
app/lib/views/schedule/widgets/member_chip.dart
Normal file
76
app/lib/views/schedule/widgets/member_chip.dart
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
/// 멤버 칩 위젯
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../core/constants.dart';
|
||||||
|
|
||||||
|
/// 멤버 칩 위젯 (일정 카드용)
|
||||||
|
class MemberChip extends StatelessWidget {
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
const MemberChip({super.key, required this.name});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [AppColors.primary, AppColors.primaryDark],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: AppColors.primary.withValues(alpha: 0.3),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 검색 결과용 멤버 칩 (작은 사이즈)
|
||||||
|
class SearchMemberChip extends StatelessWidget {
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
const SearchMemberChip({super.key, required this.name});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [AppColors.primary, AppColors.primaryDark],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: AppColors.primary.withValues(alpha: 0.3),
|
||||||
|
blurRadius: 3,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'Pretendard',
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
253
app/lib/views/schedule/widgets/schedule_card.dart
Normal file
253
app/lib/views/schedule/widgets/schedule_card.dart
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
/// 일정 카드 위젯
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../core/constants.dart';
|
||||||
|
import '../../../models/schedule.dart';
|
||||||
|
import 'member_chip.dart';
|
||||||
|
|
||||||
|
/// HTML 엔티티 디코딩
|
||||||
|
String decodeHtmlEntities(String text) {
|
||||||
|
return text
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll(''', "'")
|
||||||
|
.replaceAll(' ', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 카테고리 색상 파싱
|
||||||
|
Color parseColor(String? colorStr) {
|
||||||
|
if (colorStr == null || colorStr.isEmpty) return AppColors.textTertiary;
|
||||||
|
try {
|
||||||
|
final hex = colorStr.replaceFirst('#', '');
|
||||||
|
return Color(int.parse('FF$hex', radix: 16));
|
||||||
|
} catch (_) {
|
||||||
|
return AppColors.textTertiary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 애니메이션이 적용된 일정 카드 래퍼
|
||||||
|
class AnimatedScheduleCard extends StatefulWidget {
|
||||||
|
final int index;
|
||||||
|
final Schedule schedule;
|
||||||
|
final Color categoryColor;
|
||||||
|
|
||||||
|
const AnimatedScheduleCard({
|
||||||
|
super.key,
|
||||||
|
required this.index,
|
||||||
|
required this.schedule,
|
||||||
|
required this.categoryColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AnimatedScheduleCard> createState() => _AnimatedScheduleCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimatedScheduleCardState extends State<AnimatedScheduleCard>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _fadeAnimation;
|
||||||
|
late Animation<double> _slideAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||||
|
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
||||||
|
);
|
||||||
|
|
||||||
|
_slideAnimation = Tween<double>(begin: -10.0, end: 0.0).animate(
|
||||||
|
CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 순차적 애니메이션 (index * 30ms 딜레이)
|
||||||
|
Future.delayed(Duration(milliseconds: widget.index * 30), () {
|
||||||
|
if (mounted) _controller.forward();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Opacity(
|
||||||
|
opacity: _fadeAnimation.value,
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: Offset(_slideAnimation.value, 0),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: ScheduleCard(
|
||||||
|
schedule: widget.schedule,
|
||||||
|
categoryColor: widget.categoryColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 일정 카드 위젯
|
||||||
|
class ScheduleCard extends StatelessWidget {
|
||||||
|
final Schedule schedule;
|
||||||
|
final Color categoryColor;
|
||||||
|
|
||||||
|
const ScheduleCard({
|
||||||
|
super.key,
|
||||||
|
required this.schedule,
|
||||||
|
required this.categoryColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final memberList = schedule.memberList;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.04),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.border.withValues(alpha: 0.5),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 시간 및 카테고리 뱃지
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// 시간 뱃지
|
||||||
|
if (schedule.formattedTime != null)
|
||||||
|
Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: categoryColor,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.access_time,
|
||||||
|
size: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
schedule.formattedTime!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (schedule.formattedTime != null) const SizedBox(width: 6),
|
||||||
|
// 카테고리 뱃지
|
||||||
|
if (schedule.categoryName != null)
|
||||||
|
Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: categoryColor.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
schedule.categoryName!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: categoryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// 제목
|
||||||
|
Text(
|
||||||
|
decodeHtmlEntities(schedule.title),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 출처
|
||||||
|
if (schedule.sourceName != null &&
|
||||||
|
schedule.sourceName!.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.link,
|
||||||
|
size: 11,
|
||||||
|
color: AppColors.textTertiary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
schedule.sourceName!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: AppColors.textTertiary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// 멤버
|
||||||
|
if (memberList.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.only(top: 12),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(color: AppColors.divider, width: 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: memberList.length >= 5
|
||||||
|
? [
|
||||||
|
const MemberChip(name: '프로미스나인'),
|
||||||
|
]
|
||||||
|
: memberList
|
||||||
|
.map((name) => MemberChip(name: name))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
257
app/lib/views/schedule/widgets/search_card.dart
Normal file
257
app/lib/views/schedule/widgets/search_card.dart
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
/// 검색 결과 카드 위젯
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../core/constants.dart';
|
||||||
|
import '../../../models/schedule.dart';
|
||||||
|
import 'member_chip.dart';
|
||||||
|
import 'schedule_card.dart' show decodeHtmlEntities;
|
||||||
|
|
||||||
|
/// 검색 결과 카드 (웹과 동일한 디자인 - 왼쪽에 날짜, 오른쪽에 내용)
|
||||||
|
class SearchScheduleCard extends StatelessWidget {
|
||||||
|
final Schedule schedule;
|
||||||
|
final Color categoryColor;
|
||||||
|
|
||||||
|
const SearchScheduleCard({
|
||||||
|
super.key,
|
||||||
|
required this.schedule,
|
||||||
|
required this.categoryColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 날짜 파싱
|
||||||
|
Map<String, dynamic>? _parseDate(String? dateStr) {
|
||||||
|
if (dateStr == null) return null;
|
||||||
|
try {
|
||||||
|
final date = DateTime.parse(dateStr);
|
||||||
|
const weekdays = ['일', '월', '화', '수', '목', '금', '토'];
|
||||||
|
return {
|
||||||
|
'year': date.year,
|
||||||
|
'month': date.month,
|
||||||
|
'day': date.day,
|
||||||
|
'weekday': weekdays[date.weekday % 7],
|
||||||
|
'isSunday': date.weekday == 7,
|
||||||
|
'isSaturday': date.weekday == 6,
|
||||||
|
};
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final memberList = schedule.memberList;
|
||||||
|
final dateInfo = _parseDate(schedule.date);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.04),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.border.withValues(alpha: 0.5),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// 왼쪽 날짜 영역 (카드 높이에 맞춤)
|
||||||
|
if (dateInfo != null)
|
||||||
|
Container(
|
||||||
|
width: 72,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 6),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: AppColors.background,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(7),
|
||||||
|
bottomLeft: Radius.circular(7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// 년도
|
||||||
|
Text(
|
||||||
|
'${dateInfo['year']}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'Pretendard',
|
||||||
|
fontSize: 10,
|
||||||
|
color: AppColors.textTertiary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 월.일 (줄바꿈 방지)
|
||||||
|
FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
'${dateInfo['month']}.${dateInfo['day']}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'Pretendard',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 요일
|
||||||
|
Text(
|
||||||
|
'${dateInfo['weekday']}요일',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Pretendard',
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: dateInfo['isSunday'] == true
|
||||||
|
? Colors.red.shade500
|
||||||
|
: dateInfo['isSaturday'] == true
|
||||||
|
? Colors.blue.shade500
|
||||||
|
: AppColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 오른쪽 콘텐츠 영역
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 시간 및 카테고리 뱃지
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// 시간 뱃지
|
||||||
|
if (schedule.formattedTime != null)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: categoryColor,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.access_time,
|
||||||
|
size: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
schedule.formattedTime!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'Pretendard',
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (schedule.formattedTime != null)
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
// 카테고리 뱃지
|
||||||
|
if (schedule.categoryName != null)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: categoryColor.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
schedule.categoryName!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Pretendard',
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: categoryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// 제목
|
||||||
|
Text(
|
||||||
|
decodeHtmlEntities(schedule.title),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'Pretendard',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
// 출처 (빈 문자열이 아닌 경우에만 표시)
|
||||||
|
if (schedule.sourceName != null &&
|
||||||
|
schedule.sourceName!.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.link,
|
||||||
|
size: 12,
|
||||||
|
color: AppColors.textTertiary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
schedule.sourceName!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'Pretendard',
|
||||||
|
fontSize: 12,
|
||||||
|
color: AppColors.textTertiary,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// 멤버
|
||||||
|
if (memberList.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// divider (전체 너비)
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 1,
|
||||||
|
color: AppColors.divider,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: memberList.length >= 5
|
||||||
|
? [
|
||||||
|
const SearchMemberChip(name: '프로미스나인'),
|
||||||
|
]
|
||||||
|
: memberList
|
||||||
|
.map((name) => SearchMemberChip(name: name))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue