import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../../data/models/maintenance_dto.dart'; import '../common/theme_shadcn.dart'; import 'controllers/maintenance_controller.dart'; class MaintenanceHistoryScreen extends StatefulWidget { const MaintenanceHistoryScreen({super.key}); @override State createState() => _MaintenanceHistoryScreenState(); } class _MaintenanceHistoryScreenState extends State with SingleTickerProviderStateMixin { late TabController _tabController; DateTimeRange? _selectedDateRange; String _viewMode = 'timeline'; // timeline, table, analytics @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); // 초기 데이터 로드 WidgetsBinding.instance.addPostFrameCallback((_) { final controller = context.read(); controller.loadMaintenances(refresh: true); }); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[100], body: Column( children: [ _buildHeader(), _buildViewModeSelector(), Expanded( child: Consumer( builder: (context, controller, child) { if (controller.isLoading && controller.maintenances.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (controller.error != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 64, color: Colors.red), const SizedBox(height: 16), Text(controller.error!), const SizedBox(height: 16), ElevatedButton( onPressed: () => controller.loadMaintenances(refresh: true), child: const Text('다시 시도'), ), ], ), ); } final completedMaintenances = controller.maintenances .where((m) => m.endedAt != null) .toList(); if (completedMaintenances.isEmpty) { return _buildEmptyState(); } switch (_viewMode) { case 'timeline': return _buildTimelineView(completedMaintenances); case 'table': return _buildTableView(completedMaintenances); case 'analytics': return _buildAnalyticsView(completedMaintenances, controller); default: return _buildTimelineView(completedMaintenances); } }, ), ), ], ), ); } Widget _buildHeader() { return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.1), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '유지보수 이력', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Consumer( builder: (context, controller, child) { final completedCount = controller.maintenances .where((m) => m.endedAt != null) .length; return Text( '완료된 유지보수: $completedCount건', style: TextStyle(color: Colors.grey[600]), ); }, ), ], ), Row( children: [ TextButton.icon( onPressed: _selectDateRange, icon: const Icon(Icons.date_range), label: Text( _selectedDateRange != null ? '${DateFormat('yyyy.MM.dd').format(_selectedDateRange!.start)} - ${DateFormat('yyyy.MM.dd').format(_selectedDateRange!.end)}' : '기간 선택', ), ), if (_selectedDateRange != null) IconButton( icon: const Icon(Icons.clear), onPressed: () { setState(() { _selectedDateRange = null; }); context.read().loadMaintenances(refresh: true); }, tooltip: '필터 초기화', ), ], ), ], ), const SizedBox(height: 16), _buildSummaryCards(), ], ), ); } Widget _buildSummaryCards() { return Consumer( builder: (context, controller, child) { final completedMaintenances = controller.maintenances .where((m) => m.endedAt != null) .toList(); // 백엔드에 cost 필드가 없으므로 다른 통계 사용 final thisMonthCompleted = completedMaintenances.where((m) { final now = DateTime.now(); if (m.registeredAt == null) return false; final registeredDate = m.registeredAt; return registeredDate.year == now.year && registeredDate.month == now.month; }).length; final avgPeriod = completedMaintenances.isNotEmpty ? (completedMaintenances.map((m) => m.periodMonth ?? 0).reduce((a, b) => a + b) / completedMaintenances.length).toStringAsFixed(1) : '0'; final onsiteCount = completedMaintenances.where((m) => m.maintenanceType == 'O').length; return Row( children: [ Expanded( child: _buildSummaryCard( '총 완료 건수', completedMaintenances.length.toString(), Icons.check_circle, Colors.green, ), ), const SizedBox(width: 12), Expanded( child: _buildSummaryCard( '이번 달 완료', thisMonthCompleted.toString(), Icons.calendar_today, Colors.blue, ), ), const SizedBox(width: 12), Expanded( child: _buildSummaryCard( '평균 주기', '${avgPeriod}개월', Icons.schedule, Colors.orange, ), ), const SizedBox(width: 12), Expanded( child: _buildSummaryCard( '현장 유지보수', '$onsiteCount건', Icons.location_on, Colors.purple, ), ), ], ); }, ); } Widget _buildSummaryCard(String title, String value, IconData icon, Color color) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: color, size: 24), const SizedBox(height: 8), Text( title, style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), const SizedBox(height: 4), Text( value, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: color, ), ), ], ), ); } Widget _buildViewModeSelector() { return Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), decoration: BoxDecoration( color: Colors.white, border: Border( bottom: BorderSide(color: Colors.grey[300]!), ), ), child: Row( children: [ _buildViewModeButton('timeline', Icons.timeline, '타임라인'), const SizedBox(width: 12), _buildViewModeButton('table', Icons.table_chart, '테이블'), const SizedBox(width: 12), _buildViewModeButton('analytics', Icons.analytics, '분석'), ], ), ); } Widget _buildViewModeButton(String mode, IconData icon, String label) { final isSelected = _viewMode == mode; return Expanded( child: InkWell( onTap: () => setState(() => _viewMode = mode), child: Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: isSelected ? Theme.of(context).primaryColor : Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( icon, size: 20, color: isSelected ? Colors.white : Colors.grey[600], ), const SizedBox(width: 8), Text( label, style: TextStyle( color: isSelected ? Colors.white : Colors.grey[600], fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), ], ), ), ), ); } Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.history, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), Text( '완료된 유지보수가 없습니다', style: TextStyle( fontSize: 18, color: Colors.grey[600], ), ), ], ), ); } Widget _buildTimelineView(List maintenances) { // 날짜별로 그룹화 (endedAt 기준) final groupedByDate = >{}; for (final maintenance in maintenances) { if (maintenance.endedAt == null) continue; final endedDate = maintenance.endedAt; final dateKey = DateFormat('yyyy-MM-dd').format(endedDate); groupedByDate.putIfAbsent(dateKey, () => []).add(maintenance); } final sortedDates = groupedByDate.keys.toList() ..sort((a, b) => b.compareTo(a)); // 최신순 정렬 return ListView.builder( padding: const EdgeInsets.all(24), itemCount: sortedDates.length, itemBuilder: (context, index) { final date = sortedDates[index]; final dayMaintenances = groupedByDate[date]!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Theme.of(context).primaryColor, borderRadius: BorderRadius.circular(20), ), child: Text( DateFormat('yyyy년 MM월 dd일').format(DateTime.parse(date)), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 12), ...dayMaintenances.map((m) => _buildTimelineItem(m)), const SizedBox(height: 24), ], ); }, ); } Widget _buildTimelineItem(MaintenanceDto maintenance) { return Container( margin: const EdgeInsets.only(left: 20, bottom: 12), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], border: Border( left: BorderSide( color: Theme.of(context).primaryColor, width: 3, ), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Equipment History #${maintenance.equipmentHistoryId}', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), Text( maintenance.endedAt != null ? DateFormat('HH:mm').format(maintenance.endedAt) : '', style: TextStyle( color: Colors.grey[600], fontSize: 14, ), ), ], ), const SizedBox(height: 8), Row( children: [ Chip( label: Text( maintenance.maintenanceType == 'O' ? '현장' : '원격', style: const TextStyle(fontSize: 12), ), backgroundColor: Colors.grey[200], padding: EdgeInsets.zero, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), const SizedBox(width: 8), Text( '${maintenance.periodMonth ?? 0}개월 주기', style: TextStyle( color: Colors.grey[700], fontWeight: FontWeight.w500, ), ), ], ), const SizedBox(height: 8), Row( children: [ Icon(Icons.date_range, size: 16, color: Colors.grey[600]), const SizedBox(width: 4), Text( '시작: ${maintenance.startedAt ?? 'N/A'}', style: TextStyle(color: Colors.grey[600]), ), ], ), if (maintenance.endedAt != null) ...[ const SizedBox(height: 4), Row( children: [ Icon(Icons.check_circle, size: 16, color: Colors.green), const SizedBox(width: 4), Text( '완료: ${maintenance.endedAt}', style: TextStyle(color: Colors.green), ), ], ), ], ], ), ); } /// 헤더 셀 빌더 Widget _buildHeaderCell( String text, { required int flex, required bool useExpanded, required double minWidth, }) { final child = Container( alignment: Alignment.centerLeft, child: Text( text, style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500), ), ); if (useExpanded) { return Expanded(flex: flex, child: child); } else { return SizedBox(width: minWidth, child: child); } } /// 데이터 셀 빌더 Widget _buildDataCell( Widget child, { required int flex, required bool useExpanded, required double minWidth, }) { final container = Container( alignment: Alignment.centerLeft, child: child, ); if (useExpanded) { return Expanded(flex: flex, child: container); } else { return SizedBox(width: minWidth, child: container); } } /// 헤더 셀 리스트 List _buildHeaderCells() { return [ _buildHeaderCell('시작일', flex: 1, useExpanded: true, minWidth: 100), _buildHeaderCell('완료일', flex: 1, useExpanded: true, minWidth: 100), _buildHeaderCell('장비 이력 ID', flex: 1, useExpanded: true, minWidth: 100), _buildHeaderCell('유형', flex: 0, useExpanded: false, minWidth: 80), _buildHeaderCell('주기(개월)', flex: 0, useExpanded: false, minWidth: 100), _buildHeaderCell('등록일', flex: 1, useExpanded: true, minWidth: 100), _buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 80), ]; } /// 테이블 행 빌더 Widget _buildTableRow(MaintenanceDto maintenance, int index) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), decoration: BoxDecoration( color: index.isEven ? ShadcnTheme.muted.withValues(alpha: 0.1) : null, border: const Border( bottom: BorderSide(color: Colors.black), ), ), child: Row( children: [ _buildDataCell( Text( maintenance.startedAt != null ? DateFormat('yyyy-MM-dd').format(maintenance.startedAt) : '-', style: ShadcnTheme.bodySmall, ), flex: 1, useExpanded: true, minWidth: 100, ), _buildDataCell( Text( maintenance.endedAt != null ? DateFormat('yyyy-MM-dd').format(maintenance.endedAt) : '-', style: ShadcnTheme.bodySmall, ), flex: 1, useExpanded: true, minWidth: 100, ), _buildDataCell( Text( '#${maintenance.equipmentHistoryId}', style: ShadcnTheme.bodyMedium.copyWith( fontWeight: FontWeight.w500, ), ), flex: 1, useExpanded: true, minWidth: 100, ), _buildDataCell( ShadBadge( child: Text(maintenance.maintenanceType == 'O' ? '현장' : '원격'), ), flex: 0, useExpanded: false, minWidth: 80, ), _buildDataCell( Text( '${maintenance.periodMonth ?? 0}', style: ShadcnTheme.bodySmall, textAlign: TextAlign.center, ), flex: 0, useExpanded: false, minWidth: 100, ), _buildDataCell( Text( maintenance.registeredAt != null ? DateFormat('yyyy-MM-dd').format(maintenance.registeredAt) : '-', style: ShadcnTheme.bodySmall, ), flex: 1, useExpanded: true, minWidth: 100, ), _buildDataCell( ShadButton.ghost( size: ShadButtonSize.sm, onPressed: () => _showMaintenanceDetails(maintenance), child: const Icon(Icons.visibility, size: 16), ), flex: 0, useExpanded: false, minWidth: 80, ), ], ), ); } Widget _buildTableView(List maintenances) { return Container( margin: const EdgeInsets.all(24), decoration: BoxDecoration( border: Border.all(color: Colors.black), borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), child: Column( children: [ // 고정 헤더 Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: ShadcnTheme.muted.withValues(alpha: 0.3), border: Border(bottom: BorderSide(color: Colors.black)), ), child: Row(children: _buildHeaderCells()), ), // 스크롤 바디 Expanded( child: maintenances.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.build_circle_outlined, size: 64, color: ShadcnTheme.mutedForeground, ), const SizedBox(height: 16), Text( '완료된 정비 이력이 없습니다', style: ShadcnTheme.bodyMedium.copyWith( color: ShadcnTheme.mutedForeground, ), ), ], ), ) : ListView.builder( itemCount: maintenances.length, itemBuilder: (context, index) => _buildTableRow(maintenances[index], index), ), ), ], ), ); } Widget _buildAnalyticsView(List maintenances, MaintenanceController controller) { // 월별 비용 계산 final monthlyCosts = {}; final typeDistribution = {}; final vendorCosts = {}; for (final m in maintenances) { // 월별 비용 if (m.updatedAt == null) continue; final updatedDate = m.updatedAt!; final monthKey = DateFormat('yyyy-MM').format(updatedDate); // cost 필드가 백엔드에 없으므로 비용 계산 제거 monthlyCosts[monthKey] = (monthlyCosts[monthKey] ?? 0.0) + 0.0; // 유형별 분포 final typeKey = m.maintenanceType == 'O' ? '현장' : '원격'; typeDistribution[typeKey] = (typeDistribution[typeKey] ?? 0) + 1; // 업체별 비용 (description 필드도 백엔드에 없으므로 maintenanceType 사용) final vendorKey = m.maintenanceType == 'O' ? '현장유지보수' : '원격유지보수'; vendorCosts[vendorKey] = (vendorCosts[vendorKey] ?? 0) + 1; // 건수로 대체 } return SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 월별 비용 추이 _buildAnalyticsSection( '월별 비용 추이', Icons.show_chart, _buildMonthlyChart(monthlyCosts), ), const SizedBox(height: 24), // 유형별 분포 _buildAnalyticsSection( '유지보수 유형 분포', Icons.pie_chart, _buildTypeDistribution(typeDistribution), ), const SizedBox(height: 24), // 업체별 비용 _buildAnalyticsSection( '업체별 비용 현황', Icons.business, _buildVendorCosts(vendorCosts), ), const SizedBox(height: 24), // 통계 요약 _buildStatisticsSummary(maintenances), ], ), ); } Widget _buildAnalyticsSection(String title, IconData icon, Widget content) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(icon, color: Theme.of(context).primaryColor), const SizedBox(width: 8), Text( title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 16), content, ], ), ); } Widget _buildMonthlyChart(Map monthlyCosts) { final sortedMonths = monthlyCosts.keys.toList()..sort(); return Column( children: sortedMonths.map((month) { final cost = monthlyCosts[month]!; final maxCost = monthlyCosts.values.reduce((a, b) => a > b ? a : b); final ratio = cost / maxCost; return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( children: [ SizedBox( width: 60, child: Text( month, style: const TextStyle(fontSize: 12), ), ), Expanded( child: Stack( children: [ Container( height: 30, decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(4), ), ), FractionallySizedBox( widthFactor: ratio, child: Container( height: 30, decoration: BoxDecoration( color: Theme.of(context).primaryColor, borderRadius: BorderRadius.circular(4), ), ), ), ], ), ), const SizedBox(width: 8), Text( '₩${NumberFormat('#,###').format(cost)}', style: const TextStyle(fontSize: 12), ), ], ), ); }).toList(), ); } Widget _buildTypeDistribution(Map typeDistribution) { final total = typeDistribution.values.fold(0, (sum, count) => sum + count); return Row( children: typeDistribution.entries.map((entry) { final percentage = (entry.value / total * 100).toStringAsFixed(1); return Expanded( child: Container( padding: const EdgeInsets.all(16), margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( color: entry.key == '현장' ? Colors.blue[50] : Colors.green[50], borderRadius: BorderRadius.circular(8), border: Border.all( color: entry.key == '현장' ? Colors.blue : Colors.green, ), ), child: Column( children: [ Icon( entry.key == '현장' ? Icons.location_on : Icons.computer, color: entry.key == '현장' ? Colors.blue : Colors.green, size: 32, ), const SizedBox(height: 8), Text( entry.key, style: const TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 4), Text( '${entry.value}건', style: const TextStyle(fontSize: 18), ), Text( '$percentage%', style: TextStyle( color: Colors.grey[600], fontSize: 12, ), ), ], ), ), ); }).toList(), ); } Widget _buildVendorCosts(Map vendorCosts) { final sortedVendors = vendorCosts.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); return Column( children: sortedVendors.take(5).map((entry) { return ListTile( leading: CircleAvatar( child: Text(entry.key.substring(0, 1).toUpperCase()), ), title: Text(entry.key), trailing: Text( '₩${NumberFormat('#,###').format(entry.value)}', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), ); }).toList(), ); } Widget _buildStatisticsSummary(List maintenances) { // 비용 대신 유지보수 기간 통계로 대체 (백엔드에 cost 필드 없음) final totalMaintenances = maintenances.length; final avgPeriod = maintenances.isNotEmpty ? maintenances.map((m) => m.periodMonth).reduce((a, b) => a + b) / maintenances.length : 0.0; final maxPeriod = maintenances.isNotEmpty ? maintenances.map((m) => m.periodMonth).reduce((a, b) => a > b ? a : b).toDouble() : 0.0; final minPeriod = maintenances.isNotEmpty ? maintenances.map((m) => m.periodMonth).reduce((a, b) => a < b ? a : b).toDouble() : 0.0; return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: LinearGradient( colors: [ Theme.of(context).primaryColor, Theme.of(context).primaryColor.withValues(alpha: 0.8), ], ), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '통계 요약', style: TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildStatItem('총 건수', '$totalMaintenances건'), _buildStatItem('평균 기간', '${avgPeriod.toStringAsFixed(1)}개월'), _buildStatItem('최대 기간', '${maxPeriod.toInt()}개월'), _buildStatItem('최소 기간', '${minPeriod.toInt()}개월'), ], ), ], ), ); } Widget _buildStatItem(String label, String value) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( color: Colors.white.withValues(alpha: 0.8), fontSize: 12, ), ), const SizedBox(height: 4), Text( value, style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ); } void _selectDateRange() async { final picked = await showDateRangePicker( context: context, firstDate: DateTime(2020), lastDate: DateTime.now(), initialDateRange: _selectedDateRange, ); if (picked != null) { setState(() { _selectedDateRange = picked; }); // 날짜 범위로 필터링 // TODO: 백엔드 API가 날짜 범위 필터를 지원하면 구현 } } void _showMaintenanceDetails(MaintenanceDto maintenance) { showShadDialog( context: context, builder: (context) => ShadDialog( title: const Text('유지보수 상세 정보'), child: SizedBox( width: 500, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ _buildDetailRow('장비 이력 ID', '#${maintenance.equipmentHistoryId}'), _buildDetailRow('유지보수 유형', maintenance.maintenanceType == 'O' ? '현장' : '원격'), _buildDetailRow('시작일', maintenance.startedAt != null ? DateFormat('yyyy-MM-dd').format(maintenance.startedAt) : 'N/A'), _buildDetailRow('완료일', maintenance.endedAt != null ? DateFormat('yyyy-MM-dd').format(maintenance.endedAt) : 'N/A'), _buildDetailRow('주기', '${maintenance.periodMonth ?? 0}개월'), _buildDetailRow('등록일', maintenance.registeredAt != null ? DateFormat('yyyy-MM-dd').format(maintenance.registeredAt) : 'N/A'), ], ), ), ), actions: [ ShadButton.outline( onPressed: () => Navigator.of(context).pop(), child: const Text('닫기'), ), ], ), ); } Widget _buildDetailRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 120, child: Text( label, style: const TextStyle(fontWeight: FontWeight.bold), ), ), Expanded( child: Text(value), ), ], ), ); } void _exportHistory() { ShadToaster.of(context).show( const ShadToast( title: Text('엑셀 내보내기'), description: Text('엑셀 내보내기 기능은 준비 중입니다'), ), ); } }