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:
caadiq 2026-01-13 22:07:44 +09:00
parent fbe18b6157
commit 88f15a3ec1
4 changed files with 594 additions and 569 deletions

View file

@ -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('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&#39;', "'")
.replaceAll('&nbsp;', ' ');
}
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,
),
),
);
}
}

View 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,
),
),
);
}
}

View 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('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&#39;', "'")
.replaceAll('&nbsp;', ' ');
}
///
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(),
),
),
],
],
),
),
);
}
}

View 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(),
),
],
],
),
),
),
],
),
),
);
}
}