초기 커밋

This commit is contained in:
2026-03-01 07:55:59 +09:00
commit b0262d6bab
67 changed files with 4660 additions and 0 deletions

View 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,
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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,
),
),
),
),
),
],
),
);
}
}