feat(category): add autocomplete for category inputs

This commit is contained in:
JiWoong Sul
2025-12-01 18:02:09 +09:00
parent c1aa16c521
commit 0e75a23ade
7 changed files with 277 additions and 72 deletions

View File

@@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
import '../../../core/constants/app_colors.dart';
import '../../../core/constants/app_typography.dart';
import '../../../core/utils/category_mapper.dart';
import '../../../domain/entities/restaurant.dart';
import '../../../domain/entities/weather_info.dart';
import '../../providers/ad_provider.dart';
@@ -188,23 +189,32 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
_ensureDefaultCategorySelection(categories);
final selectedCategories =
_effectiveSelectedCategories(categories);
final isAllSelected = _isAllCategoriesSelected(
categories,
);
final categoryChips = categories
.map(
(category) => _buildCategoryChip(
category,
isDark,
selectedCategories.contains(category),
categories,
),
)
.toList();
return Wrap(
spacing: 8,
runSpacing: 8,
children: categories.isEmpty
? [const Text('카테고리 없음')]
: categories
.map(
(category) => _buildCategoryChip(
category,
isDark,
selectedCategories.contains(
category,
),
categories,
),
)
.toList(),
: [
_buildAllCategoryChip(
isDark,
isAllSelected,
categories,
),
...categoryChips,
],
);
},
loading: () => const CircularProgressIndicator(),
@@ -404,30 +414,77 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
);
}
Widget _buildAllCategoryChip(
bool isDark,
bool isSelected,
List<String> availableCategories,
) {
const icon = Icons.restaurant_menu;
return FilterChip(
label: const Text('전체'),
selected: isSelected,
onSelected: (_) => _handleToggleAllCategories(availableCategories),
avatar: _buildChipIcon(
isSelected: isSelected,
icon: icon,
color: AppColors.lightPrimary,
),
backgroundColor: isDark
? AppColors.darkSurface
: AppColors.lightBackground,
selectedColor: AppColors.lightPrimary.withValues(alpha: 0.2),
checkmarkColor: AppColors.lightPrimary,
showCheckmark: false,
labelStyle: TextStyle(
color: isSelected
? AppColors.lightPrimary
: (isDark ? AppColors.darkTextPrimary : AppColors.lightTextPrimary),
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
),
side: BorderSide(
color: isSelected
? AppColors.lightPrimary
: (isDark ? AppColors.darkDivider : AppColors.lightDivider),
),
);
}
Widget _buildCategoryChip(
String category,
bool isDark,
bool isSelected,
List<String> availableCategories,
) {
final icon = CategoryMapper.getIcon(category);
final color = CategoryMapper.getColor(category);
final activeColor = AppColors.lightPrimary;
final accentColor = isSelected ? activeColor : color;
final displayName = CategoryMapper.getDisplayName(category);
return FilterChip(
label: Text(category),
label: Text(displayName),
selected: isSelected,
onSelected: (selected) =>
_handleCategoryToggle(category, selected, availableCategories),
avatar: _buildChipIcon(
isSelected: isSelected,
icon: icon,
color: accentColor,
),
backgroundColor: isDark
? AppColors.darkSurface
: AppColors.lightBackground,
selectedColor: AppColors.lightPrimary.withValues(alpha: 0.2),
checkmarkColor: AppColors.lightPrimary,
selectedColor: activeColor.withValues(alpha: 0.2),
checkmarkColor: accentColor,
showCheckmark: false,
labelStyle: TextStyle(
color: isSelected
? AppColors.lightPrimary
? accentColor
: (isDark ? AppColors.darkTextPrimary : AppColors.lightTextPrimary),
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
),
side: BorderSide(
color: isSelected
? AppColors.lightPrimary
? accentColor
: (isDark ? AppColors.darkDivider : AppColors.lightDivider),
),
);
@@ -444,9 +501,6 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
if (isSelected) {
nextSelection.add(category);
} else {
if (nextSelection.length <= 1 && nextSelection.contains(category)) {
return;
}
nextSelection.remove(category);
}
@@ -458,6 +512,43 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
});
}
void _handleToggleAllCategories(List<String> availableCategories) {
if (availableCategories.isEmpty) return;
final shouldSelectAll = !_isAllCategoriesSelected(availableCategories);
setState(() {
_hasUserAdjustedCategories = true;
_selectedCategories
..clear()
..addAll(shouldSelectAll ? availableCategories : <String>[]);
});
}
bool _isAllCategoriesSelected(List<String> availableCategories) {
if (availableCategories.isEmpty) return false;
if (!_hasUserAdjustedCategories) return true;
final availableSet = availableCategories.toSet();
final selectedSet = _selectedCategories.toSet();
return selectedSet.isNotEmpty &&
availableSet.difference(selectedSet).isEmpty;
}
Widget _buildChipIcon({
required bool isSelected,
required IconData icon,
required Color color,
}) {
final displayedIcon = isSelected ? Icons.check : icon;
return SizedBox(
width: 20,
height: 20,
child: Icon(displayedIcon, size: 18, color: color),
);
}
int _getRestaurantCountInRange(
List<Restaurant> restaurants,
Position location,
@@ -702,18 +793,15 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
}
List<String> _effectiveSelectedCategories(List<String> availableCategories) {
if (_selectedCategories.isEmpty && !_hasUserAdjustedCategories) {
return availableCategories;
}
final availableSet = availableCategories.toSet();
final filtered = _selectedCategories
.where((category) => availableSet.contains(category))
.toList();
if (filtered.isEmpty) {
if (!_hasUserAdjustedCategories) {
return availableCategories;
}
return filtered;
}
}