458 lines
12 KiB
Dart
458 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
class OverviewPage extends StatelessWidget {
|
|
const OverviewPage({super.key});
|
|
|
|
static const navigationItems = [
|
|
(Icons.space_dashboard_outlined, '개요'),
|
|
(Icons.error_outline, '오류'),
|
|
(Icons.account_tree_outlined, '프로젝트'),
|
|
(Icons.build_outlined, '수정 작업'),
|
|
(Icons.fact_check_outlined, '검토'),
|
|
(Icons.settings_outlined, '설정'),
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final showRail = constraints.maxWidth >= 920;
|
|
final compactOrganization = constraints.maxWidth < 720;
|
|
final compactBrand = constraints.maxWidth < 420;
|
|
return Scaffold(
|
|
drawer: showRail ? null : const _NavigationDrawer(),
|
|
appBar: AppBar(
|
|
toolbarHeight: 68,
|
|
titleSpacing: showRail ? 24 : 0,
|
|
title: _Brand(compact: compactBrand),
|
|
actions: [
|
|
_OrganizationBadge(compact: compactOrganization),
|
|
const SizedBox(width: 16),
|
|
],
|
|
),
|
|
body: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (showRail) const _NavigationRail(),
|
|
const Expanded(child: _OverviewContent()),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Brand extends StatelessWidget {
|
|
const _Brand({required this.compact});
|
|
|
|
final bool compact;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const _BrandMark(),
|
|
if (!compact) ...[
|
|
const SizedBox(width: 10),
|
|
const Text(
|
|
'ARIADNE',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w800,
|
|
letterSpacing: 2.1,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BrandMark extends StatelessWidget {
|
|
const _BrandMark();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: 32,
|
|
height: 32,
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.primary,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: const Icon(Icons.route, color: Colors.white, size: 19),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _OrganizationBadge extends StatelessWidget {
|
|
const _OrganizationBadge({required this.compact});
|
|
|
|
final bool compact;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Semantics(
|
|
button: true,
|
|
enabled: false,
|
|
label: '조직 선택 기능 준비 중',
|
|
child: Tooltip(
|
|
message: '조직 선택 기능 준비 중',
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: compact ? 10 : 12,
|
|
vertical: 8,
|
|
),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: const Color(0xFFDCE1E6)),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.apartment, size: 17),
|
|
if (!compact) ...[
|
|
const SizedBox(width: 7),
|
|
const Text(
|
|
'조직 선택',
|
|
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
|
|
),
|
|
const SizedBox(width: 5),
|
|
const Icon(Icons.expand_more, size: 18),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _NavigationRail extends StatelessWidget {
|
|
const _NavigationRail();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: 224,
|
|
color: Colors.white,
|
|
padding: const EdgeInsets.fromLTRB(12, 20, 12, 12),
|
|
child: Column(
|
|
children: [
|
|
for (
|
|
var index = 0;
|
|
index < OverviewPage.navigationItems.length;
|
|
index++
|
|
)
|
|
_NavigationTile(
|
|
icon: OverviewPage.navigationItems[index].$1,
|
|
label: OverviewPage.navigationItems[index].$2,
|
|
selected: index == 0,
|
|
),
|
|
const Spacer(),
|
|
const _ConnectionStatus(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _NavigationDrawer extends StatelessWidget {
|
|
const _NavigationDrawer();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Drawer(
|
|
child: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Column(
|
|
children: [
|
|
for (
|
|
var index = 0;
|
|
index < OverviewPage.navigationItems.length;
|
|
index++
|
|
)
|
|
_NavigationTile(
|
|
icon: OverviewPage.navigationItems[index].$1,
|
|
label: OverviewPage.navigationItems[index].$2,
|
|
selected: index == 0,
|
|
),
|
|
const Spacer(),
|
|
const _ConnectionStatus(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _NavigationTile extends StatelessWidget {
|
|
const _NavigationTile({
|
|
required this.icon,
|
|
required this.label,
|
|
required this.selected,
|
|
});
|
|
|
|
final IconData icon;
|
|
final String label;
|
|
final bool selected;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final color = selected
|
|
? Theme.of(context).colorScheme.primary
|
|
: const Color(0xFF5C6870);
|
|
return Semantics(
|
|
button: true,
|
|
enabled: false,
|
|
selected: selected,
|
|
child: Container(
|
|
margin: const EdgeInsets.only(bottom: 4),
|
|
decoration: BoxDecoration(
|
|
color: selected
|
|
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.09)
|
|
: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: ListTile(
|
|
dense: true,
|
|
leading: Icon(icon, color: color, size: 21),
|
|
title: Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: color,
|
|
fontSize: 14,
|
|
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ConnectionStatus extends StatelessWidget {
|
|
const _ConnectionStatus();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF5F7F8),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: const Row(
|
|
children: [
|
|
Icon(Icons.circle, color: Color(0xFF87939B), size: 9),
|
|
SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'IOP 연결 대기',
|
|
style: TextStyle(
|
|
color: Color(0xFF5C6870),
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _OverviewContent extends StatelessWidget {
|
|
const _OverviewContent();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return CustomScrollView(
|
|
slivers: [
|
|
SliverPadding(
|
|
padding: const EdgeInsets.fromLTRB(28, 28, 28, 40),
|
|
sliver: SliverToBoxAdapter(
|
|
child: Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 1180),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text(
|
|
'오류 흐름 개요',
|
|
style: Theme.of(context).textTheme.headlineMedium,
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'조직의 오류 수집부터 분석, 수정과 병합 검토까지 한곳에서 관리합니다.',
|
|
style: TextStyle(color: Color(0xFF66727A), fontSize: 14),
|
|
),
|
|
const SizedBox(height: 24),
|
|
const _MetricGrid(),
|
|
const SizedBox(height: 22),
|
|
const _EmptyState(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _MetricGrid extends StatelessWidget {
|
|
const _MetricGrid();
|
|
|
|
static const metrics = [
|
|
(Icons.bug_report_outlined, '열린 오류', '0', Color(0xFFC24B3F)),
|
|
(Icons.manage_search_outlined, '분석 중', '0', Color(0xFF9A6A16)),
|
|
(Icons.auto_fix_high_outlined, '수정 가능', '0', Color(0xFF2458A6)),
|
|
(Icons.rate_review_outlined, '병합 검토', '0', Color(0xFF4A6A54)),
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final columns = constraints.maxWidth >= 980
|
|
? 4
|
|
: constraints.maxWidth >= 560
|
|
? 2
|
|
: 1;
|
|
final width = (constraints.maxWidth - ((columns - 1) * 14)) / columns;
|
|
|
|
return Wrap(
|
|
spacing: 14,
|
|
runSpacing: 14,
|
|
children: [
|
|
for (final metric in metrics)
|
|
SizedBox(
|
|
width: width,
|
|
child: _MetricCard(
|
|
icon: metric.$1,
|
|
label: metric.$2,
|
|
value: metric.$3,
|
|
color: metric.$4,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _MetricCard extends StatelessWidget {
|
|
const _MetricCard({
|
|
required this.icon,
|
|
required this.label,
|
|
required this.value,
|
|
required this.color,
|
|
});
|
|
|
|
final IconData icon;
|
|
final String label;
|
|
final String value;
|
|
final Color color;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
margin: EdgeInsets.zero,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(18),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 42,
|
|
height: 42,
|
|
decoration: BoxDecoration(
|
|
color: color.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(icon, color: color, size: 22),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
value,
|
|
style: const TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.w800,
|
|
height: 1,
|
|
),
|
|
),
|
|
const SizedBox(height: 7),
|
|
Text(
|
|
label,
|
|
style: const TextStyle(
|
|
color: Color(0xFF66727A),
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _EmptyState extends StatelessWidget {
|
|
const _EmptyState();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
margin: EdgeInsets.zero,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 52),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
width: 58,
|
|
height: 58,
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.primary.withValues(alpha: 0.08),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.route_outlined,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
size: 28,
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
Text(
|
|
'첫 프로젝트를 연결하세요',
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'저장소와 오류 입력을 연결하면 Ariadne가 문제의 경로를 추적합니다.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: Color(0xFF66727A), fontSize: 14),
|
|
),
|
|
const SizedBox(height: 20),
|
|
FilledButton.icon(
|
|
onPressed: null,
|
|
icon: const Icon(Icons.add, size: 18),
|
|
label: const Text('프로젝트 연결 준비 중'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|