From 1a02c15e7897addae40fa0744d2e1f1787343f74 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 4 Feb 2026 10:38:41 +0900 Subject: [PATCH] =?UTF-8?q?RP=20=ED=99=9C=EC=84=B1=ED=99=94/=EB=B9=84?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94=20UX=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/dashboard_screen.dart | 171 ++++++++++-------- 1 file changed, 97 insertions(+), 74 deletions(-) diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index 4a14e9e6..653a6697 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -164,6 +164,7 @@ class _DashboardScreenState extends ConsumerState { Future>? _linkedRpsFuture; bool _showAllActivities = false; + final Set _revokedClientIds = {}; @override void initState() { @@ -213,7 +214,9 @@ class _DashboardScreenState extends ConsumerState { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('$appName 연동이 해지되었습니다.')), ); - _refreshAll(); + setState(() { + _revokedClientIds.add(clientId); + }); } } catch (e) { if (mounted) { @@ -297,6 +300,7 @@ class _DashboardScreenState extends ConsumerState { await ref.read(profileProvider.notifier).loadProfile(); await _loadAuditLogs(reset: true); setState(() { + _revokedClientIds.clear(); _linkedRpsFuture = _fetchLinkedRps(); }); if (_linkedRpsFuture != null) { @@ -866,11 +870,13 @@ class _DashboardScreenState extends ConsumerState { List<_ActivityItem> _buildActivityItems(List linkedRps) { final items = <_ActivityItem>[]; for (final rp in linkedRps) { + final isRevoked = _revokedClientIds.contains(rp.id); final lastAuthLabel = rp.lastAuthenticatedAt != null ? _formatDateTime(rp.lastAuthenticatedAt!) : '연동됨'; + final normalizedStatus = rp.status.toLowerCase(); - final statusLabel = normalizedStatus.isEmpty || normalizedStatus == 'active' ? '활성' : '비활성'; + final statusLabel = isRevoked ? '비활성' : (normalizedStatus.isEmpty || normalizedStatus == 'active' ? '활성' : '비활성'); final name = rp.name.isNotEmpty ? rp.name : rp.id; items.add( _ActivityItem( @@ -879,7 +885,8 @@ class _DashboardScreenState extends ConsumerState { lastAuthAt: lastAuthLabel, status: statusLabel, canLogout: false, - onRevoke: () => _onRevokeLink(rp.id, name), + isRevoked: isRevoked, + onRevoke: isRevoked ? null : () => _onRevokeLink(rp.id, name), ), ); } @@ -933,85 +940,99 @@ class _DashboardScreenState extends ConsumerState { } Widget _buildActivityCard(_ActivityItem item) { - final statusColor = item.status == '활성' ? Colors.green : Colors.grey; - return Container( - width: 260, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: _surface, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: _border), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - item.appName, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _ink), + final isActive = item.status == '활성'; + final statusColor = isActive ? Colors.green : Colors.grey; + final borderColor = isActive ? Colors.green.withOpacity(0.5) : _border; + final borderWidth = isActive ? 1.5 : 1.0; + + return Opacity( + opacity: item.isRevoked ? 0.6 : 1.0, + child: Container( + width: 260, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: borderColor, width: borderWidth), + boxShadow: isActive ? [ + BoxShadow( + color: Colors.green.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ) + ] : null, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + item.appName, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _ink), + ), ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: statusColor.withOpacity(0.12), - borderRadius: BorderRadius.circular(999), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.12), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + item.status, + style: TextStyle(fontSize: 11, color: statusColor, fontWeight: FontWeight.w600), + ), ), - child: Text( - item.status, - style: TextStyle(fontSize: 11, color: statusColor, fontWeight: FontWeight.w600), - ), - ), - ], - ), - const SizedBox(height: 12), - Text( - '최근 인증', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - const SizedBox(height: 4), - Text( - item.lastAuthAt, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: _ink), - ), - const SizedBox(height: 16), - Row( - children: [ - if (item.canLogout) + ], + ), + const SizedBox(height: 12), + Text( + '최근 인증', + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + const SizedBox(height: 4), + Text( + item.lastAuthAt, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: _ink), + ), + const SizedBox(height: 16), + Row( + children: [ + if (item.canLogout) + Expanded( + child: OutlinedButton( + onPressed: item.onLogout, + style: OutlinedButton.styleFrom( + foregroundColor: _ink, + side: const BorderSide(color: _border), + padding: const EdgeInsets.symmetric(vertical: 8), + ), + child: const Text('로그아웃', style: TextStyle(fontSize: 13)), + ), + ), + if (item.canLogout) const SizedBox(width: 8), Expanded( child: OutlinedButton( - onPressed: item.onLogout, + onPressed: (_isRevoking || item.isRevoked) ? null : item.onRevoke, style: OutlinedButton.styleFrom( - foregroundColor: _ink, - side: const BorderSide(color: _border), + foregroundColor: item.isRevoked ? Colors.grey : Colors.redAccent, + side: BorderSide(color: item.isRevoked ? Colors.grey : Colors.redAccent, width: 0.5), padding: const EdgeInsets.symmetric(vertical: 8), ), - child: const Text('로그아웃', style: TextStyle(fontSize: 13)), + child: _isRevoking && !item.isRevoked + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.redAccent), + ) + : Text(item.isRevoked ? '해지됨' : '연동 해지', style: const TextStyle(fontSize: 13)), ), ), - if (item.canLogout) const SizedBox(width: 8), - Expanded( - child: OutlinedButton( - onPressed: _isRevoking ? null : item.onRevoke, - style: OutlinedButton.styleFrom( - foregroundColor: Colors.redAccent, - side: const BorderSide(color: Colors.redAccent, width: 0.5), - padding: const EdgeInsets.symmetric(vertical: 8), - ), - child: _isRevoking - ? const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.redAccent), - ) - : const Text('연동 해지', style: TextStyle(fontSize: 13)), - ), - ), - ], - ), - ], + ], + ), + ], + ), ), ); } @@ -1212,6 +1233,7 @@ class _ActivityItem { final String lastAuthAt; final String status; final bool canLogout; + final bool isRevoked; final VoidCallback? onLogout; final VoidCallback? onRevoke; @@ -1221,7 +1243,8 @@ class _ActivityItem { required this.lastAuthAt, required this.status, required this.canLogout, + this.isRevoked = false, this.onLogout, this.onRevoke, }); -} +} \ No newline at end of file