- 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>
257 lines
9.6 KiB
Dart
257 lines
9.6 KiB
Dart
/// 검색 결과 카드 위젯
|
|
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(),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|