전역 구조 리팩터링 및 테스트 확장

This commit is contained in:
JiWoong Sul
2025-09-29 01:51:47 +09:00
parent c00c0c9ab2
commit fef7108479
70 changed files with 7709 additions and 3185 deletions

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:intl/intl.dart' as intl;
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:superport_v2/core/constants/app_sections.dart';
@@ -7,7 +9,9 @@ import 'package:superport_v2/features/masters/warehouse/domain/entities/warehous
import 'package:superport_v2/features/masters/warehouse/domain/repositories/warehouse_repository.dart';
import 'package:superport_v2/widgets/app_layout.dart';
import 'package:superport_v2/widgets/components/empty_state.dart';
import 'package:superport_v2/widgets/components/feedback.dart';
import 'package:superport_v2/widgets/components/filter_bar.dart';
import 'package:superport_v2/widgets/components/superport_date_picker.dart';
class ReportingPage extends StatefulWidget {
const ReportingPage({super.key});
@@ -18,12 +22,16 @@ class ReportingPage extends StatefulWidget {
class _ReportingPageState extends State<ReportingPage> {
late final WarehouseRepository _warehouseRepository;
final DateFormat _dateFormat = DateFormat('yyyy.MM.dd');
final intl.DateFormat _dateFormat = intl.DateFormat('yyyy.MM.dd');
DateTimeRange? _dateRange;
ReportTypeFilter _selectedType = ReportTypeFilter.all;
ReportStatusFilter _selectedStatus = ReportStatusFilter.all;
WarehouseFilterOption _selectedWarehouse = WarehouseFilterOption.all;
DateTimeRange? _appliedDateRange;
DateTimeRange? _pendingDateRange;
ReportTypeFilter _appliedType = ReportTypeFilter.all;
ReportTypeFilter _pendingType = ReportTypeFilter.all;
ReportStatusFilter _appliedStatus = ReportStatusFilter.all;
ReportStatusFilter _pendingStatus = ReportStatusFilter.all;
WarehouseFilterOption _appliedWarehouse = WarehouseFilterOption.all;
WarehouseFilterOption _pendingWarehouse = WarehouseFilterOption.all;
List<WarehouseFilterOption> _warehouseOptions = const [
WarehouseFilterOption.all,
@@ -63,14 +71,14 @@ class _ReportingPageState extends State<ReportingPage> {
if (mounted) {
setState(() {
_warehouseOptions = options;
WarehouseFilterOption nextSelected = WarehouseFilterOption.all;
for (final option in options) {
if (option == _selectedWarehouse) {
nextSelected = option;
break;
}
}
_selectedWarehouse = nextSelected;
_appliedWarehouse = _resolveWarehouseOption(
_appliedWarehouse,
options,
);
_pendingWarehouse = _resolveWarehouseOption(
_pendingWarehouse,
options,
);
});
}
} catch (error) {
@@ -78,7 +86,8 @@ class _ReportingPageState extends State<ReportingPage> {
setState(() {
_warehouseError = '창고 목록을 불러오지 못했습니다. 잠시 후 다시 시도하세요.';
_warehouseOptions = const [WarehouseFilterOption.all];
_selectedWarehouse = WarehouseFilterOption.all;
_appliedWarehouse = WarehouseFilterOption.all;
_pendingWarehouse = WarehouseFilterOption.all;
});
}
} finally {
@@ -90,57 +99,70 @@ class _ReportingPageState extends State<ReportingPage> {
}
}
Future<void> _pickDateRange() async {
final now = DateTime.now();
final initialRange =
_dateRange ??
DateTimeRange(
start: DateTime(
now.year,
now.month,
now.day,
).subtract(const Duration(days: 6)),
end: DateTime(now.year, now.month, now.day),
);
final picked = await showDateRangePicker(
context: context,
initialDateRange: initialRange,
firstDate: DateTime(now.year - 5),
lastDate: DateTime(now.year + 2),
helpText: '기간 선택',
saveText: '적용',
currentDate: now,
locale: Localizations.localeOf(context),
);
if (picked != null && mounted) {
setState(() {
_dateRange = picked;
});
}
}
void _resetFilters() {
setState(() {
_dateRange = null;
_selectedType = ReportTypeFilter.all;
_selectedStatus = ReportStatusFilter.all;
_selectedWarehouse = WarehouseFilterOption.all;
_appliedDateRange = null;
_pendingDateRange = null;
_appliedType = ReportTypeFilter.all;
_pendingType = ReportTypeFilter.all;
_appliedStatus = ReportStatusFilter.all;
_pendingStatus = ReportStatusFilter.all;
_appliedWarehouse = WarehouseFilterOption.all;
_pendingWarehouse = WarehouseFilterOption.all;
});
}
void _applyFilters() {
setState(() {
_appliedDateRange = _pendingDateRange;
_appliedType = _pendingType;
_appliedStatus = _pendingStatus;
_appliedWarehouse = _pendingWarehouse;
});
}
bool get _canExport {
return _dateRange != null && _selectedType != ReportTypeFilter.all;
return _appliedDateRange != null && _appliedType != ReportTypeFilter.all;
}
bool get _hasCustomFilters {
return _dateRange != null ||
_selectedType != ReportTypeFilter.all ||
_selectedStatus != ReportStatusFilter.all ||
_selectedWarehouse != WarehouseFilterOption.all;
return _appliedDateRange != null ||
_appliedType != ReportTypeFilter.all ||
_appliedStatus != ReportStatusFilter.all ||
_appliedWarehouse != WarehouseFilterOption.all;
}
String get _dateRangeLabel {
final range = _dateRange;
bool get _hasAppliedFilters => _hasCustomFilters;
bool get _hasDirtyFilters =>
!_isSameRange(_pendingDateRange, _appliedDateRange) ||
_pendingType != _appliedType ||
_pendingStatus != _appliedStatus ||
_pendingWarehouse != _appliedWarehouse;
bool _isSameRange(DateTimeRange? a, DateTimeRange? b) {
if (identical(a, b)) {
return true;
}
if (a == null || b == null) {
return a == b;
}
return a.start == b.start && a.end == b.end;
}
WarehouseFilterOption _resolveWarehouseOption(
WarehouseFilterOption target,
List<WarehouseFilterOption> options,
) {
for (final option in options) {
if (option == target) {
return option;
}
}
return options.first;
}
String _dateRangeLabel(DateTimeRange? range) {
if (range == null) {
return '기간 선택';
}
@@ -150,11 +172,7 @@ class _ReportingPageState extends State<ReportingPage> {
String _formatDate(DateTime value) => _dateFormat.format(value);
void _handleExport(ReportExportFormat format) {
final messenger = ScaffoldMessenger.of(context);
messenger.clearSnackBars();
messenger.showSnackBar(
SnackBar(content: Text('${format.label} 다운로드 연동은 준비 중입니다.')),
);
SuperportToast.info(context, '${format.label} 다운로드 연동은 준비 중입니다.');
}
@override
@@ -174,34 +192,56 @@ class _ReportingPageState extends State<ReportingPage> {
onPressed: _canExport
? () => _handleExport(ReportExportFormat.xlsx)
: null,
leading: const Icon(LucideIcons.fileDown, size: 16),
leading: const Icon(lucide.LucideIcons.fileDown, size: 16),
child: const Text('XLSX 다운로드'),
),
ShadButton.outline(
onPressed: _canExport
? () => _handleExport(ReportExportFormat.pdf)
: null,
leading: const Icon(LucideIcons.fileText, size: 16),
leading: const Icon(lucide.LucideIcons.fileText, size: 16),
child: const Text('PDF 다운로드'),
),
],
toolbar: FilterBar(
actionConfig: FilterBarActionConfig(
onApply: _applyFilters,
onReset: _resetFilters,
hasPendingChanges: _hasDirtyFilters,
hasActiveFilters: _hasAppliedFilters,
),
children: [
ShadButton.outline(
onPressed: _pickDateRange,
leading: const Icon(LucideIcons.calendar, size: 16),
child: Text(_dateRangeLabel),
SizedBox(
width: 220,
child: SuperportDateRangePickerButton(
value: _pendingDateRange ?? _appliedDateRange,
dateFormat: _dateFormat,
firstDate: DateTime(DateTime.now().year - 5),
lastDate: DateTime(DateTime.now().year + 2),
initialDateRange:
_pendingDateRange ??
_appliedDateRange ??
DateTimeRange(
start: DateTime.now().subtract(const Duration(days: 6)),
end: DateTime.now(),
),
onChanged: (range) {
setState(() {
_pendingDateRange = range;
});
},
),
),
SizedBox(
width: 200,
child: ShadSelect<ReportTypeFilter>(
key: ValueKey(_selectedType),
initialValue: _selectedType,
key: ValueKey(_pendingType),
initialValue: _pendingType,
selectedOptionBuilder: (_, value) => Text(value.label),
onChanged: (value) {
if (value == null) return;
setState(() {
_selectedType = value;
_pendingType = value;
});
},
options: [
@@ -214,14 +254,14 @@ class _ReportingPageState extends State<ReportingPage> {
width: 220,
child: ShadSelect<WarehouseFilterOption>(
key: ValueKey(
'${_selectedWarehouse.cacheKey}-${_warehouseOptions.length}',
'${_pendingWarehouse.cacheKey}-${_warehouseOptions.length}',
),
initialValue: _selectedWarehouse,
initialValue: _pendingWarehouse,
selectedOptionBuilder: (_, value) => Text(value.label),
onChanged: (value) {
if (value == null) return;
setState(() {
_selectedWarehouse = value;
_pendingWarehouse = value;
});
},
options: [
@@ -233,13 +273,13 @@ class _ReportingPageState extends State<ReportingPage> {
SizedBox(
width: 200,
child: ShadSelect<ReportStatusFilter>(
key: ValueKey(_selectedStatus),
initialValue: _selectedStatus,
key: ValueKey(_pendingStatus),
initialValue: _pendingStatus,
selectedOptionBuilder: (_, value) => Text(value.label),
onChanged: (value) {
if (value == null) return;
setState(() {
_selectedStatus = value;
_pendingStatus = value;
});
},
options: [
@@ -248,11 +288,6 @@ class _ReportingPageState extends State<ReportingPage> {
],
),
),
ShadButton.ghost(
onPressed: _hasCustomFilters ? _resetFilters : null,
leading: const Icon(LucideIcons.rotateCcw, size: 16),
child: const Text('초기화'),
),
],
),
child: Column(
@@ -264,7 +299,7 @@ class _ReportingPageState extends State<ReportingPage> {
child: Row(
children: [
Icon(
LucideIcons.circleAlert,
lucide.LucideIcons.circleAlert,
size: 16,
color: theme.colorScheme.destructive,
),
@@ -280,7 +315,7 @@ class _ReportingPageState extends State<ReportingPage> {
const SizedBox(width: 8),
ShadButton.ghost(
onPressed: _isLoadingWarehouses ? null : _loadWarehouses,
leading: const Icon(LucideIcons.refreshCw, size: 16),
leading: const Icon(lucide.LucideIcons.refreshCw, size: 16),
child: const Text('재시도'),
),
],
@@ -290,14 +325,12 @@ class _ReportingPageState extends State<ReportingPage> {
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
children: [
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
const SizedBox(width: 8),
Text('창고 목록을 불러오는 중입니다...', style: theme.textTheme.small),
children: const [
SuperportSkeleton(width: 180, height: 20),
SizedBox(width: 12),
SuperportSkeleton(width: 140, height: 20),
SizedBox(width: 12),
SuperportSkeleton(width: 120, height: 20),
],
),
),
@@ -312,11 +345,13 @@ class _ReportingPageState extends State<ReportingPage> {
children: [
_SummaryRow(
label: '기간',
value: _dateRange == null ? '기간을 선택하세요.' : _dateRangeLabel,
value: _appliedDateRange == null
? '기간을 선택하세요.'
: _dateRangeLabel(_appliedDateRange),
),
_SummaryRow(label: '유형', value: _selectedType.label),
_SummaryRow(label: '창고', value: _selectedWarehouse.label),
_SummaryRow(label: '상태', value: _selectedStatus.label),
_SummaryRow(label: '유형', value: _appliedType.label),
_SummaryRow(label: '창고', value: _appliedWarehouse.label),
_SummaryRow(label: '상태', value: _appliedStatus.label),
if (!_canExport)
Padding(
padding: const EdgeInsets.only(top: 12),
@@ -339,9 +374,10 @@ class _ReportingPageState extends State<ReportingPage> {
),
child: SizedBox(
height: 240,
child: EmptyState(
icon: LucideIcons.chartBar,
message: '필터를 선택하고 다운로드하면 결과 미리보기가 제공됩니다.',
child: SuperportEmptyState(
icon: lucide.LucideIcons.chartBar,
title: '미리보기 데이터가 없습니다.',
description: '필터를 적용하거나 보고서를 다운로드하면 이 영역에 요약이 표시됩니다.',
),
),
),