1
0
forked from baron/baron-sso

내정보 페이지 사용성개선, adminFront user 정보 연동.

This commit is contained in:
Lectom C Han
2026-01-30 13:42:41 +09:00
parent 1cb5115f2a
commit 35552943d7
29 changed files with 1586 additions and 472 deletions

View File

@@ -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,
),

View File

@@ -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,
),

View File

@@ -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,
),

View File

@@ -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,

View File

@@ -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,
),

View File

@@ -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,

View File

@@ -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),

View File

@@ -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,

View File

@@ -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(),