1
0
forked from baron/baron-sso

프로필 자동저장 제거 및 명시적 저장 UX 개선

This commit is contained in:
2026-03-20 09:57:39 +09:00
parent 318948c2fb
commit 0bb41ae354
2 changed files with 210 additions and 184 deletions

View File

@@ -38,12 +38,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
final FocusNode _departmentFocus = FocusNode();
final FocusNode _phoneFocus = FocusNode();
final FocusNode _phoneCodeFocus = FocusNode();
bool _nameTouched = false;
bool _departmentTouched = false;
bool _phoneTouched = false;
bool _phoneCodeTouched = false;
bool _isSavingField = false;
String? _skipAutoSaveField;
String? _fieldSaveError;
String _initialPhone = '';
bool _isPhoneChanged = false;
@@ -61,10 +57,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
@override
void initState() {
super.initState();
_nameFocus.addListener(_onNameFocusChange);
_departmentFocus.addListener(_onDepartmentFocusChange);
_phoneFocus.addListener(_onPhoneFocusChange);
_phoneCodeFocus.addListener(_onPhoneCodeFocusChange);
}
void _debugLog(
@@ -83,63 +75,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_log.fine(parts.join(' '));
}
void _onNameFocusChange() {
if (!mounted) return;
if (!_nameFocus.hasFocus && _nameTouched) {
Future.microtask(() {
if (!mounted) return;
final profile = ref.read(profileProvider).value ?? _cachedProfile;
if (profile != null) _autoSaveIfEditing(profile, 'name');
});
} else if (_nameFocus.hasFocus) {
_nameTouched = true;
}
}
void _onDepartmentFocusChange() {
if (!mounted) return;
_debugLog(
'department_focus_change',
field: 'department',
hasFocus: _departmentFocus.hasFocus,
);
if (!_departmentFocus.hasFocus && _departmentTouched) {
Future.microtask(() {
if (!mounted) return;
final profile = ref.read(profileProvider).value ?? _cachedProfile;
if (profile != null) _autoSaveIfEditing(profile, 'department');
});
} else if (_departmentFocus.hasFocus) {
_departmentTouched = true;
}
}
void _onPhoneFocusChange() {
if (!mounted) return;
if (!_phoneFocus.hasFocus && _phoneTouched) {
Future.microtask(() {
if (!mounted) return;
final profile = ref.read(profileProvider).value ?? _cachedProfile;
if (profile != null) _handlePhoneFocusChange(profile);
});
} else if (_phoneFocus.hasFocus) {
_phoneTouched = true;
}
}
void _onPhoneCodeFocusChange() {
if (!mounted) return;
if (!_phoneCodeFocus.hasFocus && _phoneCodeTouched) {
Future.microtask(() {
if (!mounted) return;
final profile = ref.read(profileProvider).value ?? _cachedProfile;
if (profile != null) _handlePhoneFocusChange(profile);
});
} else if (_phoneCodeFocus.hasFocus) {
_phoneCodeTouched = true;
}
}
@override
void dispose() {
_nameController?.dispose();
@@ -210,14 +145,13 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_isCodeSent = false;
_isVerifying = false;
_codeController?.clear();
_phoneTouched = false;
_phoneCodeTouched = false;
}
void _startEditing(String field, UserProfile profile) {
_debugLog('start_editing', field: field);
setState(() {
_editingField = field;
_fieldSaveError = null;
if (field == 'name') {
_nameController?.text = profile.name;
} else if (field == 'department') {
@@ -252,8 +186,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_resetPhoneState();
}
_editingField = null;
_nameTouched = false;
_departmentTouched = false;
_fieldSaveError = null;
});
}
@@ -307,9 +240,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
SnackBar(content: Text(tr('msg.userfront.profile.phone.verified'))),
);
}
if (_editingField == 'phone') {
await _saveField(profile);
}
} catch (e) {
setState(() => _isVerifying = false);
if (mounted) {
@@ -389,64 +319,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
}
}
void _autoSaveIfEditing(UserProfile profile, String field) {
if (_editingField != field) return;
if (_skipAutoSaveField == field) {
_debugLog('autosave_skip', field: field, reason: 'skip_flag');
_skipAutoSaveField = null;
return;
}
if (_isVerifying) {
_debugLog('autosave_skip', field: field, reason: 'verifying');
return;
}
if (_isSavingField) {
_debugLog('autosave_skip', field: field, reason: 'saving_in_flight');
return;
}
if (!_hasFieldChanged(profile, field)) {
_debugLog(
'autosave_skip',
field: field,
reason: 'unchanged',
changed: false,
);
setState(() {
if (field == 'phone') {
_resetPhoneState();
}
_editingField = null;
if (field == 'name') {
_nameTouched = false;
} else if (field == 'department') {
_departmentTouched = false;
}
});
return;
}
_debugLog('autosave_trigger', field: field, changed: true);
_saveField(profile);
}
void _handlePhoneFocusChange(UserProfile profile) {
if (_editingField != 'phone') return;
if (_skipAutoSaveField == 'phone') {
_skipAutoSaveField = null;
return;
}
if (_isVerifying) return;
if (_isSavingField) return;
if (_phoneFocus.hasFocus || _phoneCodeFocus.hasFocus) return;
if (!_hasFieldChanged(profile, 'phone')) {
setState(() {
_resetPhoneState();
_editingField = null;
});
return;
}
_saveField(profile);
}
bool _hasFieldChanged(UserProfile profile, String field) {
if (field == 'name') {
return (_nameController?.text.trim() ?? '') != profile.name;
@@ -466,6 +338,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_debugLog('save_skip', reason: 'saving_in_flight');
return;
}
setState(() {
_fieldSaveError = null;
});
final currentField = _editingField!;
final nextName = currentField == 'name'
@@ -482,26 +359,24 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
if (currentField == 'name' && nextName.isEmpty) {
_debugLog('save_skip', field: currentField, reason: 'empty_name');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(tr('msg.userfront.profile.name_required'))),
);
setState(() {
_fieldSaveError = tr('msg.userfront.profile.name_required');
});
return;
}
if (currentField == 'department' && nextDepartment.isEmpty) {
_debugLog('save_skip', field: currentField, reason: 'empty_department');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tr('msg.userfront.profile.department_required')),
),
);
setState(() {
_fieldSaveError = tr('msg.userfront.profile.department_required');
});
return;
}
if (currentField == 'phone') {
if (nextPhone.isEmpty) {
_debugLog('save_skip', field: currentField, reason: 'empty_phone');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(tr('msg.userfront.profile.phone_required'))),
);
setState(() {
_fieldSaveError = tr('msg.userfront.profile.phone_required');
});
return;
}
if (_isPhoneChanged && !_isPhoneVerified) {
@@ -510,11 +385,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
field: currentField,
reason: 'phone_not_verified',
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tr('msg.userfront.profile.phone_verify_required')),
),
);
setState(() {
_fieldSaveError = tr('msg.userfront.profile.phone_verify_required');
});
return;
}
}
@@ -531,13 +404,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_resetPhoneState();
}
_editingField = null;
_nameTouched = false;
_departmentTouched = false;
});
return;
}
_isSavingField = true;
setState(() {
_isSavingField = true;
});
_debugLog('save_dispatch', field: currentField, changed: true);
try {
@@ -555,8 +429,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_resetPhoneState();
}
_editingField = null;
_nameTouched = false;
_departmentTouched = false;
});
_debugLog('save_success', field: currentField);
ScaffoldMessenger.of(context).showSnackBar(
@@ -566,19 +438,19 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
} catch (e) {
_debugLog('save_failed', field: currentField, reason: e.toString());
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
tr(
'msg.userfront.profile.update_failed',
params: {'error': e.toString()},
),
),
),
);
setState(() {
_fieldSaveError = tr(
'msg.userfront.profile.update_failed',
params: {'error': e.toString().replaceFirst('Exception: ', '')},
);
});
}
} finally {
_isSavingField = false;
if (mounted) {
setState(() {
_isSavingField = false;
});
}
}
}
@@ -793,13 +665,15 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
);
}
final hasChanged = _hasFieldChanged(profile, field);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: TextField(
@@ -807,23 +681,38 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
controller: controller,
focusNode: field == 'name' ? _nameFocus : _departmentFocus,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _autoSaveIfEditing(profile, field),
onSubmitted: (_) => _saveField(profile),
onChanged: (_) {
setState(() {
_fieldSaveError = null;
});
},
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: label,
errorText: _fieldSaveError,
),
),
),
const SizedBox(width: 12),
Listener(
onPointerDown: (_) {
_skipAutoSaveField = field;
},
child: OutlinedButton(
key: Key('profile-$field-cancel-button'),
onPressed: isUpdating ? null : () => _cancelEditing(profile),
child: Text(tr('ui.common.cancel')),
),
ElevatedButton(
key: Key('profile-$field-save-button'),
onPressed: isUpdating || !hasChanged || _isSavingField
? null
: () => _saveField(profile),
child: _isSavingField
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(tr('ui.common.save')),
),
const SizedBox(width: 8),
OutlinedButton(
key: Key('profile-$field-cancel-button'),
onPressed: isUpdating || _isSavingField ? null : () => _cancelEditing(profile),
child: Text(tr('ui.common.cancel')),
),
],
),
@@ -847,6 +736,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
);
}
final hasChanged = _hasFieldChanged(profile, 'phone');
final canSave = hasChanged && (!_isPhoneChanged || _isPhoneVerified);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -856,7 +748,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: TextField(
@@ -864,10 +756,16 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
focusNode: _phoneFocus,
keyboardType: TextInputType.phone,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _autoSaveIfEditing(profile, 'phone'),
onSubmitted: (_) => _saveField(profile),
onChanged: (_) {
setState(() {
_fieldSaveError = null;
});
},
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: '01012345678',
errorText: _fieldSaveError,
suffixIcon: _isPhoneVerified
? const Icon(Icons.check_circle, color: Colors.green)
: null,
@@ -886,14 +784,22 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
),
),
const SizedBox(width: 8),
Listener(
onPointerDown: (_) {
_skipAutoSaveField = 'phone';
},
child: OutlinedButton(
onPressed: isUpdating ? null : () => _cancelEditing(profile),
child: Text(tr('ui.common.cancel')),
),
ElevatedButton(
onPressed: isUpdating || !canSave || _isSavingField
? null
: () => _saveField(profile),
child: _isSavingField
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(tr('ui.common.save')),
),
const SizedBox(width: 8),
OutlinedButton(
onPressed: isUpdating || _isSavingField ? null : () => _cancelEditing(profile),
child: Text(tr('ui.common.cancel')),
),
],
),