Merge branch 'cursor/session-local-time-382d' — local session expiry display

Co-authored-by: okuma <o0kuma@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-08-04 05:18:17 +00:00
commit 57fa4dca3c
No known key found for this signature in database
2 changed files with 39 additions and 1 deletions

View File

@ -8,6 +8,17 @@ import '../state/session_state.dart';
class SessionsScreen extends StatefulWidget { class SessionsScreen extends StatefulWidget {
const SessionsScreen({super.key}); const SessionsScreen({super.key});
/// Formats API `expires_at` (UTC RFC3339) for the device local timezone.
static String formatExpiresAtLocal(Object? raw) {
if (raw == null) return '';
final parsed = DateTime.tryParse(raw.toString());
if (parsed == null) return raw.toString();
final local = parsed.toLocal();
String two(int n) => n.toString().padLeft(2, '0');
return '${local.year}-${two(local.month)}-${two(local.day)} '
'${two(local.hour)}:${two(local.minute)}';
}
@override @override
State<SessionsScreen> createState() => _SessionsScreenState(); State<SessionsScreen> createState() => _SessionsScreenState();
} }
@ -104,7 +115,10 @@ class _SessionsScreenState extends State<SessionsScreen> {
s['is_current'] == true ? '이 기기 (현재)' : '세션 #${s['id']}', s['is_current'] == true ? '이 기기 (현재)' : '세션 #${s['id']}',
style: theme.textTheme.titleSmall, style: theme.textTheme.titleSmall,
), ),
subtitle: Text('만료: ${s['expires_at'] ?? ''}', style: theme.textTheme.bodySmall), subtitle: Text(
'만료: ${SessionsScreen.formatExpiresAtLocal(s['expires_at'])}',
style: theme.textTheme.bodySmall,
),
trailing: IconButton( trailing: IconButton(
tooltip: '세션 종료', tooltip: '세션 종료',
icon: const Icon(Icons.logout), icon: const Icon(Icons.logout),

View File

@ -0,0 +1,24 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:ykavu_mobile/screens/sessions_screen.dart';
void main() {
test('formatExpiresAtLocal converts UTC Z to local wall clock', () {
// Fixed offset via DateTime.parse keeps instant; toLocal() uses test TZ.
final raw = '2026-09-02T09:06:45.137172Z';
final out = SessionsScreen.formatExpiresAtLocal(raw);
final expected = DateTime.parse(raw).toLocal();
final two = (int n) => n.toString().padLeft(2, '0');
expect(
out,
'${expected.year}-${two(expected.month)}-${two(expected.day)} '
'${two(expected.hour)}:${two(expected.minute)}',
);
expect(out.contains('T'), isFalse);
expect(out.endsWith('Z'), isFalse);
});
test('formatExpiresAtLocal handles null and garbage', () {
expect(SessionsScreen.formatExpiresAtLocal(null), '');
expect(SessionsScreen.formatExpiresAtLocal('not-a-date'), 'not-a-date');
});
}