초기 커밋
This commit is contained in:
238
lib/features/admin/presentation/screens/admin_home_screen.dart
Normal file
238
lib/features/admin/presentation/screens/admin_home_screen.dart
Normal file
@@ -0,0 +1,238 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../../core/router/route_names.dart';
|
||||
import '../../../../core/utils/extensions.dart';
|
||||
import '../../../../shared/widgets/chart_widgets/line_chart_widget.dart';
|
||||
|
||||
class AdminHomeScreen extends StatelessWidget {
|
||||
const AdminHomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'관리자 대시보드',
|
||||
style: context.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'시스템 현황을 한눈에 확인하세요',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
|
||||
// 통계 카드
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final crossAxisCount = context.isDesktop
|
||||
? 4
|
||||
: context.isTablet
|
||||
? 2
|
||||
: 1;
|
||||
return GridView.count(
|
||||
crossAxisCount: crossAxisCount,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 16,
|
||||
childAspectRatio: 1.8,
|
||||
children: const [
|
||||
_AdminStatCard(
|
||||
title: '전체 사용자',
|
||||
value: '1,234',
|
||||
change: '+12%',
|
||||
isPositive: true,
|
||||
icon: Icons.people,
|
||||
color: Colors.blue,
|
||||
),
|
||||
_AdminStatCard(
|
||||
title: '활성 세션',
|
||||
value: '89',
|
||||
change: '+5%',
|
||||
isPositive: true,
|
||||
icon: Icons.devices,
|
||||
color: Colors.green,
|
||||
),
|
||||
_AdminStatCard(
|
||||
title: 'API 요청/분',
|
||||
value: '2,456',
|
||||
change: '-3%',
|
||||
isPositive: false,
|
||||
icon: Icons.api,
|
||||
color: Colors.orange,
|
||||
),
|
||||
_AdminStatCard(
|
||||
title: '시스템 오류',
|
||||
value: '3',
|
||||
change: '-50%',
|
||||
isPositive: true,
|
||||
icon: Icons.error_outline,
|
||||
color: Colors.red,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(24),
|
||||
|
||||
// 차트 영역
|
||||
SizedBox(
|
||||
height: 300,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: LineChartWidget(
|
||||
title: '주간 사용자 활동',
|
||||
spots: const [
|
||||
FlSpot(0, 30),
|
||||
FlSpot(1, 45),
|
||||
FlSpot(2, 38),
|
||||
FlSpot(3, 60),
|
||||
FlSpot(4, 55),
|
||||
FlSpot(5, 70),
|
||||
FlSpot(6, 65),
|
||||
],
|
||||
bottomTitles: (value, meta) => SideTitleWidget(
|
||||
meta: meta,
|
||||
child: Text(
|
||||
['월', '화', '수', '목', '금', '토', '일']
|
||||
[value.toInt() % 7],
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
|
||||
// 빠른 링크
|
||||
Text(
|
||||
'빠른 액세스',
|
||||
style: context.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
_QuickAction(
|
||||
icon: Icons.people,
|
||||
label: '사용자 관리',
|
||||
onTap: () => context.goNamed(RouteNames.adminUsers),
|
||||
),
|
||||
_QuickAction(
|
||||
icon: Icons.dashboard,
|
||||
label: '대시보드',
|
||||
onTap: () => context.goNamed(RouteNames.adminDashboard),
|
||||
),
|
||||
_QuickAction(
|
||||
icon: Icons.settings,
|
||||
label: '시스템 설정',
|
||||
onTap: () => context.goNamed(RouteNames.adminSettings),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AdminStatCard extends StatelessWidget {
|
||||
const _AdminStatCard({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.change,
|
||||
required this.isPositive,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String value;
|
||||
final String change;
|
||||
final bool isPositive;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Icon(icon, color: color, size: 24),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
change,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: isPositive ? Colors.green : Colors.red,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickAction extends StatelessWidget {
|
||||
const _QuickAction({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ActionChip(
|
||||
avatar: Icon(icon),
|
||||
label: Text(label),
|
||||
onPressed: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
|
||||
import '../../../../app/app_providers.dart';
|
||||
import '../../../../core/utils/extensions.dart';
|
||||
|
||||
class SystemSettingsScreen extends ConsumerWidget {
|
||||
const SystemSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final themeMode = ref.watch(themeModeNotifierProvider);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'시스템 설정',
|
||||
style: context.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'시스템 환경을 설정하세요',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
|
||||
// 일반 설정
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
child: Column(
|
||||
children: [
|
||||
_SettingsSection(
|
||||
title: '일반',
|
||||
children: [
|
||||
_SettingsTile(
|
||||
icon: Icons.brightness_6,
|
||||
title: '테마',
|
||||
subtitle: _themeLabel(themeMode),
|
||||
trailing: SegmentedButton<ThemeMode>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: ThemeMode.light,
|
||||
icon: Icon(Icons.light_mode),
|
||||
label: Text('라이트'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ThemeMode.system,
|
||||
icon: Icon(Icons.brightness_auto),
|
||||
label: Text('시스템'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ThemeMode.dark,
|
||||
icon: Icon(Icons.dark_mode),
|
||||
label: Text('다크'),
|
||||
),
|
||||
],
|
||||
selected: {themeMode},
|
||||
onSelectionChanged: (modes) {
|
||||
ref
|
||||
.read(themeModeNotifierProvider.notifier)
|
||||
.setThemeMode(modes.first);
|
||||
},
|
||||
),
|
||||
),
|
||||
_SettingsTile(
|
||||
icon: Icons.language,
|
||||
title: '언어',
|
||||
subtitle: '한국어',
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
// TODO: 언어 설정
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
|
||||
_SettingsSection(
|
||||
title: '알림',
|
||||
children: [
|
||||
_SettingsTile(
|
||||
icon: Icons.notifications,
|
||||
title: '푸시 알림',
|
||||
subtitle: '활성화됨',
|
||||
trailing: Switch(
|
||||
value: true,
|
||||
onChanged: (value) {
|
||||
// TODO: 알림 설정
|
||||
},
|
||||
),
|
||||
),
|
||||
_SettingsTile(
|
||||
icon: Icons.email,
|
||||
title: '이메일 알림',
|
||||
subtitle: '활성화됨',
|
||||
trailing: Switch(
|
||||
value: true,
|
||||
onChanged: (value) {
|
||||
// TODO: 이메일 알림 설정
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
|
||||
_SettingsSection(
|
||||
title: 'API 설정',
|
||||
children: [
|
||||
_SettingsTile(
|
||||
icon: Icons.link,
|
||||
title: 'API Base URL',
|
||||
subtitle: 'http://localhost:8000/api/v1',
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
// TODO: API URL 설정
|
||||
},
|
||||
),
|
||||
_SettingsTile(
|
||||
icon: Icons.timer,
|
||||
title: '요청 타임아웃',
|
||||
subtitle: '15초',
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
// TODO: 타임아웃 설정
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
|
||||
_SettingsSection(
|
||||
title: '정보',
|
||||
children: [
|
||||
const _SettingsTile(
|
||||
icon: Icons.info_outline,
|
||||
title: '앱 버전',
|
||||
subtitle: '1.0.0',
|
||||
),
|
||||
const _SettingsTile(
|
||||
icon: Icons.code,
|
||||
title: 'Flutter',
|
||||
subtitle: 'SDK 3.8.0',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _themeLabel(ThemeMode mode) {
|
||||
return switch (mode) {
|
||||
ThemeMode.light => '라이트 모드',
|
||||
ThemeMode.dark => '다크 모드',
|
||||
ThemeMode.system => '시스템 설정',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingsSection extends StatelessWidget {
|
||||
const _SettingsSection({
|
||||
required this.title,
|
||||
required this.children,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
...children,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingsTile extends StatelessWidget {
|
||||
const _SettingsTile({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Widget? trailing;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(title),
|
||||
subtitle: Text(subtitle),
|
||||
trailing: trailing,
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:pluto_grid/pluto_grid.dart';
|
||||
|
||||
import '../../../../core/utils/extensions.dart';
|
||||
import '../../../../shared/widgets/confirm_dialog.dart';
|
||||
|
||||
class UserManagementScreen extends ConsumerStatefulWidget {
|
||||
const UserManagementScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<UserManagementScreen> createState() =>
|
||||
_UserManagementScreenState();
|
||||
}
|
||||
|
||||
class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
|
||||
late PlutoGridStateManager stateManager;
|
||||
|
||||
// Mock 데이터
|
||||
final List<Map<String, dynamic>> _mockUsers = List.generate(
|
||||
20,
|
||||
(i) => {
|
||||
'id': '${i + 1}',
|
||||
'name': '사용자 ${i + 1}',
|
||||
'email': 'user${i + 1}@example.com',
|
||||
'role': i == 0 ? 'admin' : 'user',
|
||||
'status': i % 5 == 0 ? '비활성' : '활성',
|
||||
'created_at': '2025-01-${(i + 1).toString().padLeft(2, '0')}',
|
||||
},
|
||||
);
|
||||
|
||||
List<PlutoColumn> get _columns => [
|
||||
PlutoColumn(
|
||||
title: 'ID',
|
||||
field: 'id',
|
||||
type: PlutoColumnType.text(),
|
||||
width: 60,
|
||||
enableEditingMode: false,
|
||||
),
|
||||
PlutoColumn(
|
||||
title: '이름',
|
||||
field: 'name',
|
||||
type: PlutoColumnType.text(),
|
||||
width: 150,
|
||||
),
|
||||
PlutoColumn(
|
||||
title: '이메일',
|
||||
field: 'email',
|
||||
type: PlutoColumnType.text(),
|
||||
width: 250,
|
||||
),
|
||||
PlutoColumn(
|
||||
title: '역할',
|
||||
field: 'role',
|
||||
type: PlutoColumnType.select(['admin', 'user']),
|
||||
width: 100,
|
||||
),
|
||||
PlutoColumn(
|
||||
title: '상태',
|
||||
field: 'status',
|
||||
type: PlutoColumnType.select(['활성', '비활성']),
|
||||
width: 100,
|
||||
),
|
||||
PlutoColumn(
|
||||
title: '가입일',
|
||||
field: 'created_at',
|
||||
type: PlutoColumnType.text(),
|
||||
width: 150,
|
||||
enableEditingMode: false,
|
||||
),
|
||||
];
|
||||
|
||||
List<PlutoRow> get _rows => _mockUsers
|
||||
.map(
|
||||
(user) => PlutoRow(
|
||||
cells: {
|
||||
'id': PlutoCell(value: user['id']),
|
||||
'name': PlutoCell(value: user['name']),
|
||||
'email': PlutoCell(value: user['email']),
|
||||
'role': PlutoCell(value: user['role']),
|
||||
'status': PlutoCell(value: user['status']),
|
||||
'created_at': PlutoCell(value: user['created_at']),
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
void _showCreateUserDialog() {
|
||||
final nameController = TextEditingController();
|
||||
final emailController = TextEditingController();
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('새 사용자 추가'),
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '이름',
|
||||
hintText: '사용자 이름 입력',
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
TextField(
|
||||
controller: emailController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '이메일',
|
||||
hintText: 'email@example.com',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
// TODO: API 연동
|
||||
Navigator.pop(context);
|
||||
if (mounted) {
|
||||
context.showSnackBar('사용자가 추가되었습니다');
|
||||
}
|
||||
},
|
||||
child: const Text('추가'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteUser(PlutoRow row) async {
|
||||
final confirmed = await ConfirmDialog.show(
|
||||
context,
|
||||
title: '사용자 삭제',
|
||||
message: '정말로 이 사용자를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.',
|
||||
confirmLabel: '삭제',
|
||||
isDestructive: true,
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
stateManager.removeRows([row]);
|
||||
if (mounted) {
|
||||
context.showSnackBar('사용자가 삭제되었습니다');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 헤더
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'사용자 관리',
|
||||
style: context.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
'사용자를 조회하고 관리하세요',
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: _showCreateUserDialog,
|
||||
icon: const Icon(Icons.person_add),
|
||||
label: const Text('새 사용자'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(24),
|
||||
|
||||
// 데이터 그리드
|
||||
Expanded(
|
||||
child: Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: PlutoGrid(
|
||||
columns: _columns,
|
||||
rows: _rows,
|
||||
onLoaded: (event) {
|
||||
stateManager = event.stateManager;
|
||||
},
|
||||
onRowDoubleTap: (event) {
|
||||
// TODO: 사용자 상세 정보 다이얼로그
|
||||
},
|
||||
configuration: PlutoGridConfiguration(
|
||||
style: PlutoGridStyleConfig(
|
||||
gridBackgroundColor:
|
||||
context.colorScheme.surface,
|
||||
rowColor: context.colorScheme.surface,
|
||||
activatedColor:
|
||||
context.colorScheme.primaryContainer,
|
||||
activatedBorderColor: context.colorScheme.primary,
|
||||
gridBorderColor:
|
||||
context.colorScheme.outlineVariant,
|
||||
borderColor:
|
||||
context.colorScheme.outlineVariant,
|
||||
cellTextStyle:
|
||||
context.textTheme.bodyMedium!,
|
||||
columnTextStyle:
|
||||
context.textTheme.titleSmall!.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
columnSize: const PlutoGridColumnSizeConfig(
|
||||
autoSizeMode: PlutoAutoSizeMode.scale,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user