1
0
forked from baron/baron-sso

린트 적용

This commit is contained in:
2026-02-12 10:39:47 +09:00
parent 21b9594de5
commit 74884f6616
65 changed files with 26389 additions and 1583 deletions

View File

@@ -68,8 +68,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
content: Text(
tr(
'msg.userfront.dashboard.revoke.confirm',
fallback:
'{{app}} 앱과의 연동을 해지하시겠습니까?\\n해지하면 다음 로그인 시 다시 동의가 필요합니다.',
fallback: '{{app}} 앱과의 연동을 해지하시겠습니까?\\n해지하면 다음 로그인 시 다시 동의가 필요합니다.',
params: {'app': appName},
),
),
@@ -81,8 +80,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child:
Text(tr('ui.userfront.dashboard.revoke.confirm_button', fallback: '해지하기')),
child: Text(
tr(
'ui.userfront.dashboard.revoke.confirm_button',
fallback: '해지하기',
),
),
),
],
),
@@ -158,31 +161,45 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tr('ui.userfront.dashboard.scopes.title',
fallback: '권한 (Scopes)'),
tr(
'ui.userfront.dashboard.scopes.title',
fallback: '권한 (Scopes)',
),
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
if (item.scopes.isEmpty)
Text(
tr('msg.userfront.dashboard.scopes.empty',
fallback: '요청된 권한이 없습니다.'),
tr(
'msg.userfront.dashboard.scopes.empty',
fallback: '요청된 권한이 없습니다.',
),
style: const TextStyle(color: Colors.grey),
)
else
Wrap(
spacing: 8,
runSpacing: 4,
children: item.scopes.map((s) => Chip(
label: Text(s, style: const TextStyle(fontSize: 12)),
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
)).toList(),
children: item.scopes
.map(
(s) => Chip(
label: Text(
s,
style: const TextStyle(fontSize: 12),
),
visualDensity: VisualDensity.compact,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
)
.toList(),
),
const SizedBox(height: 24),
Text(
tr('ui.userfront.dashboard.status_history',
fallback: '상태 이력'),
tr(
'ui.userfront.dashboard.status_history',
fallback: '상태 이력',
),
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
@@ -200,10 +217,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
Builder(
builder: (context) {
final statusLabel = item.status == 'active'
? tr('ui.common.status.active',
fallback: '활성')
: tr('ui.userfront.dashboard.status.revoked',
fallback: '해지됨');
? tr('ui.common.status.active', fallback: '활성')
: tr(
'ui.userfront.dashboard.status.revoked',
fallback: '해지됨',
);
return Text(
tr(
'msg.userfront.dashboard.current_status',
@@ -242,8 +260,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
children: [
ListTile(
leading: const Icon(Icons.home_outlined),
title:
Text(tr('ui.userfront.nav.dashboard', fallback: '대시보드')),
title: Text(tr('ui.userfront.nav.dashboard', fallback: '대시보드')),
selected: true,
onTap: () {
if (closeOnTap) {
@@ -254,8 +271,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
),
ListTile(
leading: const Icon(Icons.person_outline),
title:
Text(tr('ui.userfront.nav.profile', fallback: '내 정보')),
title: Text(tr('ui.userfront.nav.profile', fallback: '내 정보')),
onTap: () {
if (closeOnTap) {
Navigator.of(context).pop();
@@ -265,8 +281,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
),
ListTile(
leading: const Icon(Icons.qr_code_scanner),
title:
Text(tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔')),
title: Text(tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔')),
onTap: () {
if (closeOnTap) {
Navigator.of(context).pop();
@@ -277,8 +292,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
const Divider(),
ListTile(
leading: const Icon(Icons.logout),
title:
Text(tr('ui.userfront.nav.logout', fallback: '로그아웃')),
title: Text(tr('ui.userfront.nav.logout', fallback: '로그아웃')),
onTap: () async {
if (closeOnTap) {
Navigator.of(context).pop();
@@ -297,12 +311,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
_revokedClientIds.clear();
});
ref.invalidate(linkedRpsProvider);
await Future.wait([
ref.read(linkedRpsProvider.future),
ref.read(authTimelineProvider.notifier).refresh(),
]);
await _loadAuditLogs(reset: true);
await ref.read(linkedRpsProvider.notifier).refresh();
}
@@ -316,21 +330,18 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
Future<AuditPage> _fetchAuditLogs({String? cursor}) async {
final baseUrl = _envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
final queryParameters = <String, String>{
'limit': '20',
};
final queryParameters = <String, String>{'limit': '20'};
if (cursor != null && cursor.isNotEmpty) {
queryParameters['cursor'] = cursor;
}
final url = Uri.parse('$baseUrl/api/v1/audit/auth/timeline')
.replace(queryParameters: queryParameters);
final url = Uri.parse(
'$baseUrl/api/v1/audit/auth/timeline',
).replace(queryParameters: queryParameters);
final useCookie = AuthTokenStore.usesCookie();
final token = AuthTokenStore.getToken();
final client = createHttpClient(withCredentials: useCookie);
final headers = <String, String>{
'Content-Type': 'application/json',
};
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null) {
headers['Authorization'] = 'Bearer $token';
}
@@ -401,11 +412,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
if (parts.length != 3) {
return null;
}
final payload = utf8.decode(base64Url.decode(base64Url.normalize(parts[1])));
final payload = utf8.decode(
base64Url.decode(base64Url.normalize(parts[1])),
);
final data = json.decode(payload) as Map<String, dynamic>;
final iatValue = data['iat'] ?? data['auth_time'];
if (iatValue is num) {
return DateTime.fromMillisecondsSinceEpoch(iatValue.toInt() * 1000).toLocal();
return DateTime.fromMillisecondsSinceEpoch(
iatValue.toInt() * 1000,
).toLocal();
}
} catch (_) {
return null;
@@ -467,9 +482,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
Widget _buildAuthMethodCell(AuditLogEntry log, String authMethod) {
final isOidc = authMethod.contains('OIDC');
if (authMethod != 'QR' && !isOidc) {
final approvedUserAgent = log.detailMap['approved_user_agent']?.toString() ?? '';
final approvedUserAgent =
log.detailMap['approved_user_agent']?.toString() ?? '';
final approvedIp = log.detailMap['approved_ip']?.toString() ?? '';
final hasApproverMeta = approvedUserAgent.isNotEmpty || approvedIp.isNotEmpty;
final hasApproverMeta =
approvedUserAgent.isNotEmpty || approvedIp.isNotEmpty;
if (!authMethod.startsWith('링크') || !hasApproverMeta) {
return _selectableText(authMethod);
}
@@ -497,7 +514,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
),
);
}
final approvedSessionId = (log.detailMap['approved_session_id']?.toString().trim().isNotEmpty ?? false)
final approvedSessionId =
(log.detailMap['approved_session_id']?.toString().trim().isNotEmpty ??
false)
? log.detailMap['approved_session_id'].toString()
: log.sessionId;
final tooltipLabel = isOidc
@@ -544,8 +563,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
isOidc ? authMethod : tr('ui.common.qr', fallback: 'QR'),
style: TextStyle(
color: approvedSessionId.isEmpty ? _ink : Colors.blueAccent,
decoration:
approvedSessionId.isEmpty ? null : TextDecoration.underline,
decoration: approvedSessionId.isEmpty
? null
: TextDecoration.underline,
),
),
),
@@ -555,9 +575,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
Widget _buildAuthMethodLine(AuditLogEntry log, String authMethod) {
final isOidc = authMethod.contains('OIDC');
if (authMethod != 'QR' && !isOidc) {
final approvedUserAgent = log.detailMap['approved_user_agent']?.toString() ?? '';
final approvedUserAgent =
log.detailMap['approved_user_agent']?.toString() ?? '';
final approvedIp = log.detailMap['approved_ip']?.toString() ?? '';
final hasApproverMeta = approvedUserAgent.isNotEmpty || approvedIp.isNotEmpty;
final hasApproverMeta =
approvedUserAgent.isNotEmpty || approvedIp.isNotEmpty;
if (!authMethod.startsWith('링크') || !hasApproverMeta) {
return _selectableText(
tr(
@@ -595,7 +617,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
),
);
}
final approvedSessionId = (log.detailMap['approved_session_id']?.toString().trim().isNotEmpty ?? false)
final approvedSessionId =
(log.detailMap['approved_session_id']?.toString().trim().isNotEmpty ??
false)
? log.detailMap['approved_session_id'].toString()
: log.sessionId;
final tooltipLabel = isOidc
@@ -642,7 +666,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
'msg.userfront.dashboard.auth_method',
fallback: '인증수단: {{method}}',
params: {
'method': isOidc ? authMethod : tr('ui.common.qr', fallback: 'QR'),
'method': isOidc
? authMethod
: tr('ui.common.qr', fallback: 'QR'),
},
),
style: TextStyle(
@@ -714,7 +740,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
final profileState = ref.watch(profileProvider);
final profile = profileState.value;
final timelineState = ref.watch(authTimelineProvider);
final userName = profile?.name ??
final userName =
profile?.name ??
profile?.email ??
profile?.phone ??
tr('ui.userfront.profile.user_fallback', fallback: 'User');
@@ -751,7 +778,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
),
],
),
drawer: isWide ? null : Drawer(child: _buildSideMenu(context, closeOnTap: true)),
drawer: isWide
? null
: Drawer(child: _buildSideMenu(context, closeOnTap: true)),
body: Row(
children: [
if (isWide)
@@ -775,11 +804,18 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMobile) ...[
_buildHeaderCard(userName, department, sessionIssuedAt),
_buildHeaderCard(
userName,
department,
sessionIssuedAt,
),
const SizedBox(height: 28),
],
_buildSectionTitle(
tr('ui.userfront.sections.apps', fallback: '나의 App 현황'),
tr(
'ui.userfront.sections.apps',
fallback: '나의 App 현황',
),
tr(
'msg.userfront.sections.apps_subtitle',
fallback: '현재 연결된 앱과 최근 인증 상태입니다.',
@@ -810,7 +846,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
);
}
Widget _buildHeaderCard(String userName, String department, DateTime? issuedAt) {
Widget _buildHeaderCard(
String userName,
String department,
DateTime? issuedAt,
) {
final sessionLabel = issuedAt != null
? _formatDateTime(issuedAt)
: tr('ui.userfront.session.unknown', fallback: '알 수 없음');
@@ -823,7 +863,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
fallback: '안녕하세요, {{name}}님',
params: {'name': userName},
),
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: _ink),
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: _ink,
),
),
const SizedBox(height: 6),
Text(
@@ -871,13 +915,14 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
children: [
Text(
title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: _ink),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: _ink,
),
),
const SizedBox(width: 12),
Text(
subtitle,
style: TextStyle(fontSize: 13, color: Colors.grey[600]),
),
Text(subtitle, style: TextStyle(fontSize: 13, color: Colors.grey[600])),
],
);
}
@@ -897,7 +942,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
const SizedBox(width: 6),
Text(
label,
style: const TextStyle(fontSize: 12, color: _ink, fontWeight: FontWeight.w600),
style: const TextStyle(
fontSize: 12,
color: _ink,
fontWeight: FontWeight.w600,
),
),
],
),
@@ -920,7 +969,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
'msg.userfront.dashboard.activities.empty',
fallback: '연동된 앱이 없습니다.',
),
style: TextStyle(fontSize: 14, color: Colors.grey[700], fontWeight: FontWeight.w600),
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
Text(
@@ -959,14 +1012,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
);
}
List<_ActivityItem> _buildActivityItems(List<LinkedRp> linkedRps) {
final items = <_ActivityItem>[];
for (final rp in linkedRps) {
final normalizedStatus = rp.status.toLowerCase();
// status가 'inactive'로 내려올 수 있으므로 이를 반영
final isActiveInApi = normalizedStatus == 'active' || normalizedStatus == '';
final isActiveInApi =
normalizedStatus == 'active' || normalizedStatus == '';
final isRevoked = !isActiveInApi;
final lastAuthLabel = rp.lastAuthenticatedAt != null
@@ -975,7 +1027,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
final statusCode = isRevoked ? 'revoked' : 'active';
final name = rp.name.isNotEmpty ? rp.name : rp.id;
items.add(
_ActivityItem(
clientId: rp.id,
@@ -995,17 +1047,17 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
items.sort((a, b) {
final aActive = a.status == 'active';
final bActive = b.status == 'active';
if (aActive && !bActive) return -1;
if (!aActive && bActive) return 1;
// 둘 다 활성이거나 둘 다 비활성인 경우 최근 인증순 내림차순
if (a.lastAuthDateTime != null && b.lastAuthDateTime != null) {
return b.lastAuthDateTime!.compareTo(a.lastAuthDateTime!);
}
if (a.lastAuthDateTime != null) return -1;
if (b.lastAuthDateTime != null) return 1;
return 0;
});
@@ -1018,7 +1070,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
return LayoutBuilder(
builder: (context, constraints) {
final maxWidth = constraints.maxWidth;
// 화면 너비에 따른 컬럼 수 및 초기 표시 개수 결정
int crossAxisCount;
if (maxWidth > 1200) {
@@ -1042,7 +1094,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
// 카드의 너비를 화면 너비에 맞춰 계산 (여백 고려)
final double spacing = 12.0;
final double cardWidth = (maxWidth - (spacing * (crossAxisCount - 1))) / crossAxisCount;
final double cardWidth =
(maxWidth - (spacing * (crossAxisCount - 1))) / crossAxisCount;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -1063,18 +1116,24 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
child: Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: () => setState(() => _showAllActivities = !_showAllActivities),
onPressed: () => setState(
() => _showAllActivities = !_showAllActivities,
),
icon: Icon(
_showAllActivities ? Icons.keyboard_arrow_up : Icons.add,
size: 18,
color: _showAllActivities ? Colors.grey : Colors.blueAccent,
color: _showAllActivities
? Colors.grey
: Colors.blueAccent,
),
label: Text(
_showAllActivities
? tr('ui.common.collapse', fallback: '접기')
: tr('ui.common.show_more', fallback: '+ 더보기'),
style: TextStyle(
color: _showAllActivities ? Colors.grey : Colors.blueAccent,
color: _showAllActivities
? Colors.grey
: Colors.blueAccent,
fontWeight: FontWeight.bold,
),
),
@@ -1090,7 +1149,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
Widget _buildActivityCard(_ActivityItem item, {double? cardWidth}) {
final isActive = item.status == 'active';
final statusColor = isActive ? Colors.green : Colors.grey;
final borderColor = isActive ? Colors.green.withValues(alpha: 128) : _border;
final borderColor = isActive
? Colors.green.withValues(alpha: 128)
: _border;
final borderWidth = isActive ? 1.5 : 1.0;
// 활성 상태면 클릭 가능 (URL 유무와 관계없이)
@@ -1104,13 +1165,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
color: _surface,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: borderColor, width: borderWidth),
boxShadow: isActive ? [
BoxShadow(
color: Colors.green.withValues(alpha: 13),
blurRadius: 10,
offset: const Offset(0, 4),
)
] : null,
boxShadow: isActive
? [
BoxShadow(
color: Colors.green.withValues(alpha: 13),
blurRadius: 10,
offset: const Offset(0, 4),
),
]
: null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -1120,7 +1183,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
Expanded(
child: Text(
item.appName,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _ink),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: _ink,
),
),
),
Container(
@@ -1132,8 +1199,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
child: Text(
item.status == 'active'
? tr('ui.common.status.active', fallback: '활성')
: tr('ui.userfront.dashboard.status.revoked', fallback: '해지됨'),
style: TextStyle(fontSize: 11, color: statusColor, fontWeight: FontWeight.w600),
: tr(
'ui.userfront.dashboard.status.revoked',
fallback: '해지됨',
),
style: TextStyle(
fontSize: 11,
color: statusColor,
fontWeight: FontWeight.w600,
),
),
),
],
@@ -1146,7 +1220,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
const SizedBox(height: 4),
Text(
item.lastAuthAt,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: _ink),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: _ink,
),
),
const SizedBox(height: 16),
Row(
@@ -1168,22 +1246,38 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
const SizedBox(width: 8),
Expanded(
child: OutlinedButton(
onPressed: (_isRevoking || item.isRevoked) ? null : item.onRevoke,
onPressed: (_isRevoking || item.isRevoked)
? null
: item.onRevoke,
style: OutlinedButton.styleFrom(
foregroundColor: item.isRevoked ? Colors.grey : Colors.redAccent,
side: BorderSide(color: item.isRevoked ? Colors.grey : Colors.redAccent, width: 0.5),
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: _isRevoking && !item.isRevoked
? const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.redAccent),
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.redAccent,
),
)
: Text(
item.isRevoked
? tr('ui.userfront.dashboard.status.revoked', fallback: '해지됨')
: tr('ui.userfront.dashboard.revoke.title', fallback: '연동 해지'),
? tr(
'ui.userfront.dashboard.status.revoked',
fallback: '해지됨',
)
: tr(
'ui.userfront.dashboard.revoke.title',
fallback: '연동 해지',
),
style: const TextStyle(fontSize: 13),
),
),
@@ -1268,7 +1362,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
),
const SizedBox(height: 8),
TextButton(
onPressed: () => ref.read(authTimelineProvider.notifier).refresh(),
onPressed: () =>
ref.read(authTimelineProvider.notifier).refresh(),
child: Text(tr('ui.common.retry', fallback: '다시 시도')),
),
],
@@ -1326,7 +1421,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
columns: [
DataColumn(
label: Text(
tr('ui.userfront.audit.table.session_id', fallback: 'Session ID'),
tr(
'ui.userfront.audit.table.session_id',
fallback: 'Session ID',
),
),
),
DataColumn(
@@ -1336,7 +1434,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
),
DataColumn(
label: Text(
tr('ui.userfront.audit.table.app', fallback: '애플리케이션'),
tr(
'ui.userfront.audit.table.app',
fallback: '애플리케이션',
),
),
),
DataColumn(
@@ -1346,17 +1447,26 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
),
DataColumn(
label: Text(
tr('ui.userfront.audit.table.device', fallback: '접속환경'),
tr(
'ui.userfront.audit.table.device',
fallback: '접속환경',
),
),
),
DataColumn(
label: Text(
tr('ui.userfront.audit.table.auth_method', fallback: '인증수단'),
tr(
'ui.userfront.audit.table.auth_method',
fallback: '인증수단',
),
),
),
DataColumn(
label: Text(
tr('ui.userfront.audit.table.result', fallback: '인증결과'),
tr(
'ui.userfront.audit.table.result',
fallback: '인증결과',
),
),
),
DataColumn(
@@ -1369,47 +1479,57 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
final statusLabel = log.status == 'success'
? tr('ui.common.status.success', fallback: '성공')
: tr('ui.common.status.failure', fallback: '실패');
final statusColor =
log.status == 'success' ? Colors.green : Colors.redAccent;
final statusColor = log.status == 'success'
? Colors.green
: Colors.redAccent;
final authMethod = log.authMethod.isNotEmpty
? log.authMethod
: _authMethodLabel();
final deviceLabel = _deviceLabelFromUserAgent(log.userAgent);
return DataRow(cells: [
DataCell(
_selectableText(
log.sessionId.isEmpty
? tr('ui.common.hyphen', fallback: '-')
: log.sessionId,
),
),
DataCell(_selectableText(_formatDateTime(log.timestamp))),
DataCell(_buildAppCell(log)),
DataCell(
_selectableText(
log.ipAddress.isEmpty
? tr('ui.common.hyphen', fallback: '-')
: log.ipAddress,
),
),
DataCell(_selectableText(deviceLabel)),
DataCell(_buildAuthMethodCell(log, authMethod)),
DataCell(
_selectableText(
statusLabel,
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.w600,
final deviceLabel = _deviceLabelFromUserAgent(
log.userAgent,
);
return DataRow(
cells: [
DataCell(
_selectableText(
log.sessionId.isEmpty
? tr('ui.common.hyphen', fallback: '-')
: log.sessionId,
),
),
),
DataCell(
_selectableText(
tr('ui.userfront.audit.table.pending', fallback: '(준비중)'),
style: const TextStyle(color: Colors.grey),
DataCell(
_selectableText(_formatDateTime(log.timestamp)),
),
),
]);
DataCell(_buildAppCell(log)),
DataCell(
_selectableText(
log.ipAddress.isEmpty
? tr('ui.common.hyphen', fallback: '-')
: log.ipAddress,
),
),
DataCell(_selectableText(deviceLabel)),
DataCell(_buildAuthMethodCell(log, authMethod)),
DataCell(
_selectableText(
statusLabel,
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.w600,
),
),
),
DataCell(
_selectableText(
tr(
'ui.userfront.audit.table.pending',
fallback: '(준비중)',
),
style: const TextStyle(color: Colors.grey),
),
),
],
);
}).toList(),
),
),
@@ -1443,7 +1563,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
Expanded(
child: _buildAppCell(
log,
style: const TextStyle(fontWeight: FontWeight.w600, color: _ink),
style: const TextStyle(
fontWeight: FontWeight.w600,
color: _ink,
),
),
),
_selectableText(
@@ -1451,7 +1574,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
? tr('ui.common.status.success', fallback: '성공')
: tr('ui.common.status.failure', fallback: '실패'),
style: TextStyle(
color: log.status == 'success' ? Colors.green : Colors.redAccent,
color: log.status == 'success'
? Colors.green
: Colors.redAccent,
fontWeight: FontWeight.w600,
),
),
@@ -1491,10 +1616,17 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
tr(
'msg.userfront.audit.device',
fallback: '접속환경: {{value}}',
params: {'value': _deviceLabelFromUserAgent(log.userAgent)},
params: {
'value': _deviceLabelFromUserAgent(log.userAgent),
},
),
),
_buildAuthMethodLine(log, log.authMethod.isNotEmpty ? log.authMethod : _authMethodLabel()),
_buildAuthMethodLine(
log,
log.authMethod.isNotEmpty
? log.authMethod
: _authMethodLabel(),
),
_selectableText(
tr(
'msg.userfront.audit.result',
@@ -1507,10 +1639,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
),
),
_selectableText(
tr(
'msg.userfront.audit.status',
fallback: '현황: (준비중)',
),
tr('msg.userfront.audit.status', fallback: '현황: (준비중)'),
style: TextStyle(color: Colors.grey[600]),
),
],
@@ -1542,7 +1671,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
),
),
TextButton(
onPressed: () => ref.read(authTimelineProvider.notifier).loadMore(),
onPressed: () =>
ref.read(authTimelineProvider.notifier).loadMore(),
child: Text(tr('ui.common.retry', fallback: '재시도')),
),
],