forked from baron/baron-sso
내정보 페이지 사용성개선, adminFront user 정보 연동.
This commit is contained in:
BIN
userfront/assets/fonts/NotoSansKR-Bold.ttf
Normal file
BIN
userfront/assets/fonts/NotoSansKR-Bold.ttf
Normal file
Binary file not shown.
BIN
userfront/assets/fonts/NotoSansKR-Regular.ttf
Normal file
BIN
userfront/assets/fonts/NotoSansKR-Regular.ttf
Normal file
Binary file not shown.
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../../core/services/auth_proxy_service.dart';
|
||||
|
||||
class ForgotPasswordScreen extends StatefulWidget {
|
||||
@@ -91,7 +90,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
children: [
|
||||
Text(
|
||||
"비밀번호를 잊으셨나요?",
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:descope/descope.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
@@ -47,6 +46,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
int _linkExpireSeconds = 0;
|
||||
Timer? _linkExpireTimer;
|
||||
bool _verificationOnly = false;
|
||||
bool _verificationApproved = false;
|
||||
String _verificationMessage = '';
|
||||
bool _drySendEnabled = false;
|
||||
|
||||
@override
|
||||
@@ -352,15 +353,72 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_onLoginSuccess(token, provider: provider);
|
||||
}
|
||||
|
||||
bool _hasLocalSession() {
|
||||
if (AuthTokenStore.getToken() != null) {
|
||||
return true;
|
||||
}
|
||||
return AuthTokenStore.usesCookie();
|
||||
}
|
||||
|
||||
void _markVerificationApproved(String message) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_verificationApproved = true;
|
||||
_verificationMessage = message;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildVerificationResultView() {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.check_circle_outline, color: Colors.green, size: 72),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'승인 완료',
|
||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.green),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_verificationMessage.isEmpty ? '로그인 승인에 성공했습니다.' : _verificationMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton(
|
||||
onPressed: () => context.go('/'),
|
||||
child: const Text('확인'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _verifyToken(String token) async {
|
||||
debugPrint("[Auth] Starting verification for token: $token");
|
||||
try {
|
||||
// Use Backend to verify the token (Backend-Driven Flow)
|
||||
await AuthProxyService.verifyMagicLink(token);
|
||||
final res = await AuthProxyService.verifyMagicLink(token);
|
||||
debugPrint("[Auth] Verification successful for token: $token");
|
||||
final jwt = res['token'] ?? res['sessionJwt'];
|
||||
final provider = res['provider'] as String?;
|
||||
final hasLocalSession = _hasLocalSession();
|
||||
|
||||
if (jwt is String && jwt.isNotEmpty) {
|
||||
if (hasLocalSession) {
|
||||
_markVerificationApproved("승인되었습니다. 이미 로그인된 브라우저입니다.");
|
||||
return;
|
||||
}
|
||||
_completeLoginFromToken(jwt, provider: provider);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
_showInfo("승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.");
|
||||
_markVerificationApproved("승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.");
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[Auth] Verification FAILED for token: $token. Error: $e");
|
||||
@@ -382,23 +440,26 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
final jwt = res['sessionJwt'] ?? res['token'];
|
||||
final status = res['status']?.toString();
|
||||
debugPrint("[Auth] Code verification successful for loginId: $sanitizedLoginId");
|
||||
final hasLocalSession = _hasLocalSession();
|
||||
|
||||
if (jwt == null && status == 'approved') {
|
||||
if (mounted) {
|
||||
_showInfo("승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (_verificationOnly) {
|
||||
if (mounted) {
|
||||
_showInfo("승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.");
|
||||
_markVerificationApproved("승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (jwt is String && jwt.isNotEmpty) {
|
||||
if (hasLocalSession) {
|
||||
_markVerificationApproved("승인되었습니다. 이미 로그인된 브라우저입니다.");
|
||||
return;
|
||||
}
|
||||
_completeLoginFromToken(jwt, provider: res['provider'] as String?);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_verificationOnly && mounted) {
|
||||
_markVerificationApproved("승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.");
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[Auth] Code verification FAILED for loginId: $sanitizedLoginId. Error: $e");
|
||||
@@ -417,23 +478,26 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
final jwt = res['sessionJwt'] ?? res['token'];
|
||||
final status = res['status']?.toString();
|
||||
debugPrint("[Auth] Short code verification successful");
|
||||
final hasLocalSession = _hasLocalSession();
|
||||
|
||||
if (jwt == null && status == 'approved') {
|
||||
if (mounted) {
|
||||
_showInfo("승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (_verificationOnly) {
|
||||
if (mounted) {
|
||||
_showInfo("승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.");
|
||||
_markVerificationApproved("승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (jwt is String && jwt.isNotEmpty) {
|
||||
if (hasLocalSession) {
|
||||
_markVerificationApproved("승인되었습니다. 이미 로그인된 브라우저입니다.");
|
||||
return;
|
||||
}
|
||||
_completeLoginFromToken(jwt, provider: res['provider'] as String?);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_verificationOnly && mounted) {
|
||||
_markVerificationApproved("승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.");
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[Auth] Short code verification FAILED. Error: $e");
|
||||
@@ -784,6 +848,19 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_verificationOnly && _verificationApproved) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('로그인 승인'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.go('/'),
|
||||
),
|
||||
),
|
||||
body: _buildVerificationResultView(),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
@@ -800,7 +877,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
children: [
|
||||
Text(
|
||||
"Baron 통합로그인",
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class LoginSuccessScreen extends StatelessWidget {
|
||||
const LoginSuccessScreen({super.key});
|
||||
@@ -18,7 +17,7 @@ class LoginSuccessScreen extends StatelessWidget {
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
"로그인 완료",
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
||||
@@ -206,7 +206,7 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
||||
MobileScanner(
|
||||
controller: controller,
|
||||
onDetect: _onDetect,
|
||||
errorBuilder: (context, error, child) {
|
||||
errorBuilder: (context, error) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/services/auth_proxy_service.dart';
|
||||
|
||||
@@ -150,7 +149,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
children: [
|
||||
Text(
|
||||
"새로운 비밀번호 설정",
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/services/auth_proxy_service.dart';
|
||||
|
||||
@@ -332,7 +331,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text('서비스 이용을 위해\n약관에 동의해주세요',
|
||||
style: GoogleFonts.outfit(
|
||||
style: TextStyle(
|
||||
fontSize: 20, fontWeight: FontWeight.bold, height: 1.3)),
|
||||
const SizedBox(height: 24),
|
||||
// 모두 동의 버튼
|
||||
@@ -597,7 +596,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text('본인 확인을 위해\n인증을 진행해주세요', style: GoogleFonts.outfit(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
Text('본인 확인을 위해\n인증을 진행해주세요', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
// 가족사 이메일 안내 문구
|
||||
Container(
|
||||
@@ -713,7 +712,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text('회원님의\n소속 정보를 알려주세요', style: GoogleFonts.outfit(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
Text('회원님의\n소속 정보를 알려주세요', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
@@ -826,7 +825,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text('마지막으로\n비밀번호를 설정해주세요', style: GoogleFonts.outfit(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
Text('마지막으로\n비밀번호를 설정해주세요', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
// 비밀번호 정책 안내 박스
|
||||
Container(
|
||||
@@ -918,7 +917,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text('회원가입', style: GoogleFonts.outfit(fontWeight: FontWeight.bold)),
|
||||
title: Text('회원가입', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:descope/descope.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import '../../../../core/notifiers/auth_notifier.dart';
|
||||
import '../../../../core/services/auth_token_store.dart';
|
||||
@@ -84,6 +83,45 @@ class AuditLogEntry {
|
||||
}
|
||||
}
|
||||
|
||||
class LinkedRp {
|
||||
final String id;
|
||||
final String name;
|
||||
final String logo;
|
||||
final String status;
|
||||
final List<String> scopes;
|
||||
final DateTime? lastAuthenticatedAt;
|
||||
|
||||
LinkedRp({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.logo,
|
||||
required this.status,
|
||||
required this.scopes,
|
||||
required this.lastAuthenticatedAt,
|
||||
});
|
||||
|
||||
factory LinkedRp.fromJson(Map<String, dynamic> json) {
|
||||
final rawLastAuth = json['lastAuthenticatedAt']?.toString() ?? '';
|
||||
DateTime? parsedLastAuth;
|
||||
if (rawLastAuth.isNotEmpty) {
|
||||
try {
|
||||
parsedLastAuth = DateTime.parse(rawLastAuth).toLocal();
|
||||
} catch (_) {
|
||||
parsedLastAuth = null;
|
||||
}
|
||||
}
|
||||
|
||||
return LinkedRp(
|
||||
id: json['id']?.toString() ?? '',
|
||||
name: json['name']?.toString() ?? '',
|
||||
logo: json['logo']?.toString() ?? '',
|
||||
status: json['status']?.toString() ?? '',
|
||||
scopes: (json['scopes'] as List?)?.whereType<String>().toList() ?? [],
|
||||
lastAuthenticatedAt: parsedLastAuth,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DashboardScreen extends ConsumerStatefulWidget {
|
||||
const DashboardScreen({super.key});
|
||||
|
||||
@@ -98,12 +136,14 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
static const _subtle = Color(0xFFF7F8FA);
|
||||
|
||||
Future<List<AuditLogEntry>>? _auditFuture;
|
||||
Future<List<LinkedRp>>? _linkedRpsFuture;
|
||||
bool _showAllActivities = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_auditFuture = _fetchAuditLogs();
|
||||
_linkedRpsFuture = _fetchLinkedRps();
|
||||
}
|
||||
|
||||
Future<void> _logout() async {
|
||||
@@ -172,10 +212,14 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
await ref.read(profileProvider.notifier).loadProfile();
|
||||
setState(() {
|
||||
_auditFuture = _fetchAuditLogs();
|
||||
_linkedRpsFuture = _fetchLinkedRps();
|
||||
});
|
||||
if (_auditFuture != null) {
|
||||
await _auditFuture;
|
||||
}
|
||||
if (_linkedRpsFuture != null) {
|
||||
await _linkedRpsFuture;
|
||||
}
|
||||
}
|
||||
|
||||
static String _envOrDefault(String key, String fallback) {
|
||||
@@ -216,6 +260,37 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
return logs;
|
||||
}
|
||||
|
||||
Future<List<LinkedRp>> _fetchLinkedRps() async {
|
||||
final baseUrl = _envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
|
||||
final url = Uri.parse('$baseUrl/api/v1/user/rp/linked');
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
final token = AuthTokenStore.getToken();
|
||||
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
final response = await client.get(url, headers: headers);
|
||||
client.close();
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to load linked rps');
|
||||
}
|
||||
|
||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final items = (body['items'] as List?) ?? [];
|
||||
final linkedRps = items
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(LinkedRp.fromJson)
|
||||
.toList();
|
||||
|
||||
return linkedRps;
|
||||
}
|
||||
|
||||
DateTime? _getJwtIssuedAt() {
|
||||
final token = AuthTokenStore.getToken();
|
||||
if (token == null || token.isEmpty) {
|
||||
@@ -391,7 +466,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'Baron 통합로그인',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: _surface,
|
||||
@@ -442,7 +517,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
],
|
||||
_buildSectionTitle('활동상황', '현재 연결된 앱과 최근 인증 상태입니다.'),
|
||||
const SizedBox(height: 12),
|
||||
_buildActivityGrid(sessionIssuedAt, isMobile),
|
||||
_buildActivitySection(isMobile),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle('접속이력', 'Baron 통합로그인 기준의 최근 접근 기록입니다.'),
|
||||
const SizedBox(height: 12),
|
||||
@@ -545,16 +620,52 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivityGrid(DateTime? signupAt, bool isMobile) {
|
||||
final signupLabel = signupAt != null ? _formatDateTime(signupAt) : '확인 필요';
|
||||
final activities = [
|
||||
_ActivityItem(
|
||||
appName: 'Baron 통합로그인',
|
||||
lastAuthAt: signupLabel,
|
||||
status: '활성',
|
||||
canLogout: true,
|
||||
onLogout: _logout,
|
||||
),
|
||||
Widget _buildActivitySection(bool isMobile) {
|
||||
return FutureBuilder<List<LinkedRp>>(
|
||||
future: _linkedRpsFuture,
|
||||
builder: (context, snapshot) {
|
||||
final activities = _buildActivityItems(snapshot.data ?? []);
|
||||
final grid = _buildActivityGrid(activities, isMobile);
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
grid,
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'연동 정보를 불러오지 못했습니다.',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return grid;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<_ActivityItem> _buildActivityItems(List<LinkedRp> linkedRps) {
|
||||
final items = <_ActivityItem>[];
|
||||
for (final rp in linkedRps) {
|
||||
final lastAuthLabel = rp.lastAuthenticatedAt != null
|
||||
? _formatDateTime(rp.lastAuthenticatedAt!)
|
||||
: '연동됨';
|
||||
final normalizedStatus = rp.status.toLowerCase();
|
||||
final statusLabel = normalizedStatus.isEmpty || normalizedStatus == 'active' ? '활성' : '비활성';
|
||||
final name = rp.name.isNotEmpty ? rp.name : rp.id;
|
||||
items.add(
|
||||
_ActivityItem(
|
||||
appName: name,
|
||||
lastAuthAt: lastAuthLabel,
|
||||
status: statusLabel,
|
||||
canLogout: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
items.addAll([
|
||||
_ActivityItem(
|
||||
appName: 'BEPs',
|
||||
lastAuthAt: '연동 필요',
|
||||
@@ -579,7 +690,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
status: '미연동',
|
||||
canLogout: false,
|
||||
),
|
||||
];
|
||||
]);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
Widget _buildActivityGrid(List<_ActivityItem> activities, bool isMobile) {
|
||||
|
||||
if (!isMobile) {
|
||||
return Wrap(
|
||||
@@ -660,7 +776,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'가입일시',
|
||||
'최근 인증',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:descope/descope.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../../../core/notifiers/auth_notifier.dart';
|
||||
import '../../../../core/services/auth_token_store.dart';
|
||||
import '../../../../core/ui/layout_breakpoints.dart';
|
||||
@@ -28,6 +27,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
TextEditingController? _phoneController;
|
||||
TextEditingController? _departmentController;
|
||||
TextEditingController? _codeController;
|
||||
final FocusNode _nameFocus = FocusNode();
|
||||
final FocusNode _departmentFocus = FocusNode();
|
||||
final FocusNode _phoneFocus = FocusNode();
|
||||
final FocusNode _phoneCodeFocus = FocusNode();
|
||||
|
||||
String _initialPhone = '';
|
||||
bool _isPhoneChanged = false;
|
||||
@@ -41,6 +44,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_phoneController?.dispose();
|
||||
_departmentController?.dispose();
|
||||
_codeController?.dispose();
|
||||
_nameFocus.dispose();
|
||||
_departmentFocus.dispose();
|
||||
_phoneFocus.dispose();
|
||||
_phoneCodeFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -153,7 +160,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _verifyCode() async {
|
||||
Future<void> _verifyCode(UserProfile profile) async {
|
||||
final phone = _phoneController?.text ?? '';
|
||||
final code = _codeController?.text ?? '';
|
||||
if (code.isEmpty) return;
|
||||
@@ -170,6 +177,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
const SnackBar(content: Text('인증되었습니다.')),
|
||||
);
|
||||
}
|
||||
if (_editingField == 'phone') {
|
||||
await _saveField(profile);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _isVerifying = false);
|
||||
if (mounted) {
|
||||
@@ -180,6 +190,19 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _autoSaveIfEditing(UserProfile profile, String field) {
|
||||
if (_editingField != field) return;
|
||||
if (_isVerifying) return;
|
||||
_saveField(profile);
|
||||
}
|
||||
|
||||
void _handlePhoneFocusChange(UserProfile profile) {
|
||||
if (_editingField != 'phone') return;
|
||||
if (_isVerifying) return;
|
||||
if (_phoneFocus.hasFocus || _phoneCodeFocus.hasFocus) return;
|
||||
_saveField(profile);
|
||||
}
|
||||
|
||||
Future<void> _saveField(UserProfile profile) async {
|
||||
if (_editingField == null) return;
|
||||
|
||||
@@ -431,28 +454,24 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: label,
|
||||
Focus(
|
||||
focusNode: field == 'name' ? _nameFocus : _departmentFocus,
|
||||
onFocusChange: (hasFocus) {
|
||||
if (!hasFocus) {
|
||||
_autoSaveIfEditing(profile, field);
|
||||
}
|
||||
},
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
focusNode: field == 'name' ? _nameFocus : _departmentFocus,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => _autoSaveIfEditing(profile, field),
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: label,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: isUpdating ? null : () => _cancelEditing(profile),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: isUpdating ? null : () => _saveField(profile),
|
||||
child: const Text('확인'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -482,17 +501,28 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: '01012345678',
|
||||
suffixIcon: _isPhoneVerified
|
||||
? const Icon(Icons.check_circle, color: Colors.green)
|
||||
: null,
|
||||
child: Focus(
|
||||
focusNode: _phoneFocus,
|
||||
onFocusChange: (hasFocus) {
|
||||
if (!hasFocus) {
|
||||
_handlePhoneFocusChange(profile);
|
||||
}
|
||||
},
|
||||
child: TextField(
|
||||
controller: _phoneController,
|
||||
focusNode: _phoneFocus,
|
||||
keyboardType: TextInputType.phone,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => _autoSaveIfEditing(profile, 'phone'),
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: '01012345678',
|
||||
suffixIcon: _isPhoneVerified
|
||||
? const Icon(Icons.check_circle, color: Colors.green)
|
||||
: null,
|
||||
),
|
||||
enabled: !_isPhoneVerified,
|
||||
),
|
||||
enabled: !_isPhoneVerified,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -509,18 +539,29 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _codeController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: '인증번호 6자리',
|
||||
child: Focus(
|
||||
focusNode: _phoneCodeFocus,
|
||||
onFocusChange: (hasFocus) {
|
||||
if (!hasFocus) {
|
||||
_handlePhoneFocusChange(profile);
|
||||
}
|
||||
},
|
||||
child: TextField(
|
||||
controller: _codeController,
|
||||
focusNode: _phoneCodeFocus,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => _verifyCode(profile),
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: '인증번호 6자리',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: _isVerifying ? null : _verifyCode,
|
||||
onPressed: _isVerifying ? null : () => _verifyCode(profile),
|
||||
child: const Text('확인'),
|
||||
),
|
||||
],
|
||||
@@ -534,21 +575,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
style: TextStyle(color: Colors.orange, fontSize: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: isUpdating ? null : () => _cancelEditing(profile),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: isUpdating ? null : () => _saveField(profile),
|
||||
child: const Text('확인'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -556,59 +582,69 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
Widget _buildContent(UserProfile profile, bool isUpdating) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.read(profileProvider.notifier).loadProfile(),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
children: [
|
||||
_buildHeaderCard(profile),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle('기본 정보', '계정 기본 정보를 관리합니다.'),
|
||||
const SizedBox(height: 12),
|
||||
_buildCard(
|
||||
Column(
|
||||
children: [
|
||||
_buildEditableTile(
|
||||
field: 'name',
|
||||
label: '이름',
|
||||
value: profile.name,
|
||||
profile: profile,
|
||||
isUpdating: isUpdating,
|
||||
controller: _nameController!,
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile('이메일', profile.email),
|
||||
const Divider(height: 24),
|
||||
_buildPhoneEditor(profile, isUpdating),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle('조직 정보', '소속 및 구분 정보입니다.'),
|
||||
const SizedBox(height: 12),
|
||||
_buildCard(
|
||||
Column(
|
||||
children: [
|
||||
_buildEditableTile(
|
||||
field: 'department',
|
||||
label: '소속',
|
||||
value: profile.department,
|
||||
profile: profile,
|
||||
isUpdating: isUpdating,
|
||||
controller: _departmentController!,
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile('구분', profile.affiliationType),
|
||||
if (profile.companyCode.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile('회사코드', profile.companyCode),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 1200),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
children: [
|
||||
_buildHeaderCard(profile),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle('기본 정보', '계정 기본 정보를 관리합니다.'),
|
||||
const SizedBox(height: 12),
|
||||
_buildCard(
|
||||
Column(
|
||||
children: [
|
||||
_buildEditableTile(
|
||||
field: 'name',
|
||||
label: '이름',
|
||||
value: profile.name,
|
||||
profile: profile,
|
||||
isUpdating: isUpdating,
|
||||
controller: _nameController!,
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile('이메일', profile.email),
|
||||
const Divider(height: 24),
|
||||
_buildPhoneEditor(profile, isUpdating),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle('조직 정보', '소속 및 구분 정보입니다.'),
|
||||
const SizedBox(height: 12),
|
||||
_buildCard(
|
||||
Column(
|
||||
children: [
|
||||
_buildEditableTile(
|
||||
field: 'department',
|
||||
label: '소속',
|
||||
value: profile.department,
|
||||
profile: profile,
|
||||
isUpdating: isUpdating,
|
||||
controller: _departmentController!,
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile('구분', profile.affiliationType),
|
||||
if (profile.companyCode.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile('회사코드', profile.companyCode),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isUpdating || _isVerifying) ...[
|
||||
const SizedBox(height: 24),
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isUpdating || _isVerifying) ...[
|
||||
const SizedBox(height: 24),
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -652,7 +688,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'Baron 통합로그인',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: _surface,
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:descope/descope.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_web_plugins/url_strategy.dart';
|
||||
import 'features/auth/presentation/login_screen.dart';
|
||||
import 'features/auth/presentation/signup_screen.dart';
|
||||
@@ -23,6 +23,18 @@ import 'package:logging/logging.dart';
|
||||
|
||||
final _log = Logger('Main');
|
||||
|
||||
Future<void> _loadBundledFonts() async {
|
||||
const family = 'NotoSansKR';
|
||||
final loader = FontLoader(family);
|
||||
try {
|
||||
loader.addFont(rootBundle.load('assets/fonts/NotoSansKR-Regular.ttf'));
|
||||
loader.addFont(rootBundle.load('assets/fonts/NotoSansKR-Bold.ttf'));
|
||||
await loader.load();
|
||||
} catch (e) {
|
||||
_log.warning("Failed to preload bundled fonts: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
usePathUrlStrategy();
|
||||
@@ -51,6 +63,9 @@ void main() async {
|
||||
// 0. Initialize Logger
|
||||
LoggerService.init();
|
||||
|
||||
// 폰트를 먼저 로딩해서 렌더링 깨짐(FOIT/FOUT) 최소화
|
||||
await _loadBundledFonts();
|
||||
|
||||
// Initialize Descope (프로젝트 ID가 없으면 경고만 남기고 진행)
|
||||
final projectId = dotenv.maybeGet('DESCOPE_PROJECT_ID') ?? '';
|
||||
if (projectId.isEmpty || projectId == 'your-project-id') {
|
||||
@@ -228,7 +243,7 @@ class BaronSSOApp extends StatelessWidget {
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
useMaterial3: true,
|
||||
textTheme: GoogleFonts.interTextTheme(),
|
||||
fontFamily: 'NotoSansKR',
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.android: NoTransitionsBuilder(),
|
||||
|
||||
@@ -117,10 +117,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: descope
|
||||
sha256: da73578c619aefb82411ddca3e61423006a36900ff8cdc4b4ef42c4863f8ad36
|
||||
sha256: cae7d22e47d7d1c35d5a1cdfad2ae5f3b5e755476f215ddfa4dbeab6937f3ac2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.11"
|
||||
version: "0.9.12"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -133,10 +133,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.1.5"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -170,10 +170,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_riverpod
|
||||
sha256: "9e2d6907f12cc7d23a846847615941bddee8709bf2bfd274acdf5e80bcf22fde"
|
||||
sha256: a3cd0547353c1990bf5ad64f73143e5ce7a780409639559ad83a743ff3b945e4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.2.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -184,10 +184,10 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
userfront_server_client:
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: userfront_server_client
|
||||
name: frontend_server_client
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
@@ -208,14 +208,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "17.0.1"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_fonts
|
||||
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -260,10 +252,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
version: "4.10.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -296,8 +288,16 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logger
|
||||
sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.2"
|
||||
logging:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
@@ -336,6 +336,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
mobile_scanner:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mobile_scanner
|
||||
sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.1.4"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -360,62 +368,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.22"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -440,14 +392,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
qr:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: qr
|
||||
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
qr_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: qr_flutter
|
||||
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod
|
||||
sha256: c406de02bff19d920b832bddfb8283548bfa05ce41c59afba57ce643e116aa59
|
||||
sha256: "9026676260f31bb5279cc5e59ca292145d8bab4aabede9aa2555c2a626ec66f1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.2.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -633,10 +601,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||
sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.4.2"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -665,10 +633,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: f52385d4f73589977c80797e60fe51014f7f2b957b5e9a62c3f6ada439889249
|
||||
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -701,14 +669,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -719,4 +679,4 @@ packages:
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.10.4 <4.0.0"
|
||||
flutter: ">=3.35.0"
|
||||
flutter: ">=3.38.0"
|
||||
|
||||
@@ -38,13 +38,12 @@ dependencies:
|
||||
go_router: ^17.0.1
|
||||
descope: ^0.9.11
|
||||
http: ^1.6.0
|
||||
google_fonts: ^6.3.3
|
||||
flutter_dotenv: ^5.1.0
|
||||
flutter_dotenv: ^6.0.0
|
||||
url_launcher: ^6.3.2
|
||||
logging: ^1.2.0
|
||||
logger: ^2.0.0
|
||||
qr_flutter: ^4.1.0
|
||||
mobile_scanner: ^6.0.0
|
||||
mobile_scanner: ^7.1.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -96,3 +95,9 @@ flutter:
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/to/font-from-package
|
||||
fonts:
|
||||
- family: NotoSansKR
|
||||
fonts:
|
||||
- asset: assets/fonts/NotoSansKR-Regular.ttf
|
||||
- asset: assets/fonts/NotoSansKR-Bold.ttf
|
||||
weight: 700
|
||||
|
||||
Reference in New Issue
Block a user