Flutter 앱: 일정 화면 1단계 구현 (날짜 선택기 + 일정 목록)
- 자체 툴바 (년월 표시, 이전/다음 월 버튼) - 가로 스크롤 날짜 선택기 (일정 점 표시, 자동 중앙 스크롤) - 일정 카드 (시간, 카테고리, 제목, 출처, 멤버) - 순차적 페이드 인 애니메이션 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7a1e04b6ae
commit
221aaa2bb4
2 changed files with 644 additions and 53 deletions
|
|
@ -16,43 +16,46 @@ class MainShell extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final location = GoRouterState.of(context).uri.path;
|
||||
final isMembersPage = location == '/members';
|
||||
final isSchedulePage = location.startsWith('/schedule');
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
// 앱바 (툴바) - 멤버 페이지에서는 그림자 제거 (인디케이터와 계단 효과 방지)
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(56),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: isMembersPage
|
||||
? null
|
||||
: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
// 앱바 (툴바) - 일정 페이지는 자체 툴바 사용, 멤버 페이지는 그림자 제거
|
||||
appBar: isSchedulePage
|
||||
? null
|
||||
: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(56),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: isMembersPage
|
||||
? null
|
||||
: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SizedBox(
|
||||
height: 56,
|
||||
child: Center(
|
||||
child: Text(
|
||||
_getTitle(location),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Pretendard',
|
||||
color: AppColors.primary,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SizedBox(
|
||||
height: 56,
|
||||
child: Center(
|
||||
child: Text(
|
||||
_getTitle(location),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Pretendard',
|
||||
color: AppColors.primary,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 콘텐츠
|
||||
body: child,
|
||||
// 바텀 네비게이션
|
||||
|
|
|
|||
|
|
@ -2,40 +2,628 @@
|
|||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../core/constants.dart';
|
||||
import '../../models/schedule.dart';
|
||||
import '../../services/schedules_service.dart';
|
||||
|
||||
class ScheduleView extends StatelessWidget {
|
||||
/// HTML 엔티티 디코딩
|
||||
String decodeHtmlEntities(String text) {
|
||||
return text
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'")
|
||||
.replaceAll(' ', ' ');
|
||||
}
|
||||
|
||||
class ScheduleView extends StatefulWidget {
|
||||
const ScheduleView({super.key});
|
||||
|
||||
@override
|
||||
State<ScheduleView> createState() => _ScheduleViewState();
|
||||
}
|
||||
|
||||
class _ScheduleViewState extends State<ScheduleView> {
|
||||
DateTime _selectedDate = DateTime.now();
|
||||
List<Schedule> _schedules = [];
|
||||
bool _isLoading = true;
|
||||
final ScrollController _dateScrollController = ScrollController();
|
||||
final ScrollController _contentScrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSchedules();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_dateScrollController.dispose();
|
||||
_contentScrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 월별 일정 로드
|
||||
Future<void> _loadSchedules() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final schedules = await getSchedules(
|
||||
_selectedDate.year,
|
||||
_selectedDate.month,
|
||||
);
|
||||
setState(() {
|
||||
_schedules = schedules;
|
||||
_isLoading = false;
|
||||
});
|
||||
// 선택된 날짜로 스크롤
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollToSelectedDate();
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 선택된 날짜로 스크롤
|
||||
void _scrollToSelectedDate() {
|
||||
if (!_dateScrollController.hasClients) return;
|
||||
|
||||
final dayIndex = _selectedDate.day - 1;
|
||||
const itemWidth = 52.0; // 44 + 8 (gap)
|
||||
final targetOffset = (dayIndex * itemWidth) -
|
||||
(MediaQuery.of(context).size.width / 2) + (itemWidth / 2);
|
||||
_dateScrollController.animateTo(
|
||||
targetOffset.clamp(0, _dateScrollController.position.maxScrollExtent),
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
|
||||
/// 월 변경
|
||||
void _changeMonth(int delta) {
|
||||
final newDate = DateTime(_selectedDate.year, _selectedDate.month + delta, 1);
|
||||
final today = DateTime.now();
|
||||
|
||||
// 이번 달이면 오늘 날짜, 다른 달이면 1일 선택
|
||||
final selectedDay = (newDate.year == today.year && newDate.month == today.month)
|
||||
? today.day
|
||||
: 1;
|
||||
|
||||
setState(() {
|
||||
_selectedDate = DateTime(newDate.year, newDate.month, selectedDay);
|
||||
});
|
||||
_loadSchedules();
|
||||
}
|
||||
|
||||
/// 날짜 선택
|
||||
void _selectDate(DateTime date) {
|
||||
// 일정 목록 맨 위로 즉시 이동 (애니메이션과 겹치지 않도록)
|
||||
if (_contentScrollController.hasClients) {
|
||||
_contentScrollController.jumpTo(0);
|
||||
}
|
||||
setState(() => _selectedDate = date);
|
||||
// 선택된 날짜 중앙으로 스크롤
|
||||
_scrollToSelectedDate();
|
||||
}
|
||||
|
||||
/// 해당 달의 모든 날짜 배열
|
||||
List<DateTime> get _daysInMonth {
|
||||
final year = _selectedDate.year;
|
||||
final month = _selectedDate.month;
|
||||
final lastDay = DateTime(year, month + 1, 0).day;
|
||||
return List.generate(lastDay, (i) => DateTime(year, month, i + 1));
|
||||
}
|
||||
|
||||
/// 선택된 날짜의 일정
|
||||
List<Schedule> get _selectedDateSchedules {
|
||||
final dateStr = DateFormat('yyyy-MM-dd').format(_selectedDate);
|
||||
return _schedules.where((s) => s.date.split('T')[0] == dateStr).toList();
|
||||
}
|
||||
|
||||
/// 특정 날짜의 일정 (점 표시용)
|
||||
List<Schedule> _getDaySchedules(DateTime date) {
|
||||
final dateStr = DateFormat('yyyy-MM-dd').format(date);
|
||||
return _schedules.where((s) => s.date.split('T')[0] == dateStr).take(3).toList();
|
||||
}
|
||||
|
||||
/// 요일 이름
|
||||
String _getDayName(DateTime date) {
|
||||
const days = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
return days[date.weekday % 7];
|
||||
}
|
||||
|
||||
/// 오늘 여부
|
||||
bool _isToday(DateTime date) {
|
||||
final today = DateTime.now();
|
||||
return date.year == today.year &&
|
||||
date.month == today.month &&
|
||||
date.day == today.day;
|
||||
}
|
||||
|
||||
/// 선택된 날짜 여부
|
||||
bool _isSelected(DateTime date) {
|
||||
return date.year == _selectedDate.year &&
|
||||
date.month == _selectedDate.month &&
|
||||
date.day == _selectedDate.day;
|
||||
}
|
||||
|
||||
/// 카테고리 색상 파싱
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today_outlined,
|
||||
size: 64,
|
||||
return Column(
|
||||
children: [
|
||||
// 자체 툴바
|
||||
_buildToolbar(),
|
||||
// 날짜 선택기
|
||||
_buildDateSelector(),
|
||||
// 일정 목록
|
||||
Expanded(
|
||||
child: _buildScheduleList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 툴바 빌드
|
||||
Widget _buildToolbar() {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Container(
|
||||
height: 56,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
// 달력 아이콘 (2단계에서 구현)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// TODO: 달력 팝업
|
||||
},
|
||||
icon: const Icon(Icons.calendar_today_outlined, size: 20),
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
// 이전 월
|
||||
IconButton(
|
||||
onPressed: () => _changeMonth(-1),
|
||||
icon: const Icon(Icons.chevron_left, size: 24),
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
// 년월 표시
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${_selectedDate.year}년 ${_selectedDate.month}월',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 다음 월
|
||||
IconButton(
|
||||
onPressed: () => _changeMonth(1),
|
||||
icon: const Icon(Icons.chevron_right, size: 24),
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
// 검색 아이콘 (3단계에서 구현)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// TODO: 검색 모드
|
||||
},
|
||||
icon: const Icon(Icons.search, size: 20),
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 날짜 선택기 빌드
|
||||
Widget _buildDateSelector() {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: Container(
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListView.builder(
|
||||
controller: _dateScrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
itemCount: _daysInMonth.length,
|
||||
itemBuilder: (context, index) {
|
||||
final date = _daysInMonth[index];
|
||||
final isSelected = _isSelected(date);
|
||||
final isToday = _isToday(date);
|
||||
final dayOfWeek = date.weekday;
|
||||
final daySchedules = _getDaySchedules(date);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _selectDate(date),
|
||||
child: Container(
|
||||
width: 44,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.primary : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 요일
|
||||
Text(
|
||||
_getDayName(date),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isSelected
|
||||
? Colors.white.withValues(alpha: 0.8)
|
||||
: dayOfWeek == 7
|
||||
? Colors.red.shade400
|
||||
: dayOfWeek == 6
|
||||
? Colors.blue.shade400
|
||||
: AppColors.textTertiary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// 날짜
|
||||
Text(
|
||||
'${date.day}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: isToday
|
||||
? AppColors.primary
|
||||
: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// 일정 점 (최대 3개)
|
||||
SizedBox(
|
||||
height: 6,
|
||||
child: !isSelected && daySchedules.isNotEmpty
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: daySchedules.map((schedule) {
|
||||
return Container(
|
||||
width: 4,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: _parseColor(schedule.categoryColor),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 일정 목록 빌드
|
||||
Widget _buildScheduleList() {
|
||||
if (_isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.primary),
|
||||
);
|
||||
}
|
||||
|
||||
if (_selectedDateSchedules.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'${_selectedDate.month}월 ${_selectedDate.day}일 일정이 없습니다',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.textTertiary,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'일정',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
controller: _contentScrollController,
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _selectedDateSchedules.length,
|
||||
itemBuilder: (context, index) {
|
||||
final schedule = _selectedDateSchedules[index];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: index < _selectedDateSchedules.length - 1 ? 12 : 0),
|
||||
child: _AnimatedScheduleCard(
|
||||
key: ValueKey('${schedule.id}_${_selectedDate.toString()}'),
|
||||
index: index,
|
||||
schedule: schedule,
|
||||
categoryColor: _parseColor(schedule.categoryColor),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'일정 화면 준비 중',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: 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),
|
||||
);
|
||||
|
||||
// 웹과 동일: 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue