233 lines
6.8 KiB
Dart
233 lines
6.8 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:intl/intl.dart';
|
||
import 'base_text_field.dart';
|
||
import '../../../l10n/app_localizations.dart';
|
||
|
||
/// 통화 입력 필드 위젯
|
||
/// KRW/JPY(정수), USD/CNY(소수점 2자리)를 지원하며 자동 포맷팅을 제공합니다.
|
||
class CurrencyInputField extends StatefulWidget {
|
||
final TextEditingController controller;
|
||
final String currency; // 'KRW' | 'USD' | 'JPY' | 'CNY'
|
||
final String? label;
|
||
final String? hintText;
|
||
final Function(double?)? onChanged;
|
||
final String? Function(String?)? validator;
|
||
final FocusNode? focusNode;
|
||
final TextInputAction? textInputAction;
|
||
final Function()? onEditingComplete;
|
||
final bool enabled;
|
||
|
||
const CurrencyInputField({
|
||
super.key,
|
||
required this.controller,
|
||
required this.currency,
|
||
this.label,
|
||
this.hintText,
|
||
this.onChanged,
|
||
this.validator,
|
||
this.focusNode,
|
||
this.textInputAction,
|
||
this.onEditingComplete,
|
||
this.enabled = true,
|
||
});
|
||
|
||
@override
|
||
State<CurrencyInputField> createState() => _CurrencyInputFieldState();
|
||
}
|
||
|
||
class _CurrencyInputFieldState extends State<CurrencyInputField> {
|
||
late FocusNode _focusNode;
|
||
bool _isFormatted = false;
|
||
bool _isPostFrameUpdating = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_focusNode = widget.focusNode ?? FocusNode();
|
||
_focusNode.addListener(_onFocusChanged);
|
||
|
||
// 초기값이 있으면 포맷팅 적용
|
||
if (widget.controller.text.isNotEmpty) {
|
||
final value = double.tryParse(widget.controller.text.replaceAll(',', ''));
|
||
if (value != null) {
|
||
widget.controller.text = _formatCurrency(value);
|
||
_isFormatted = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
if (widget.focusNode == null) {
|
||
_focusNode.dispose();
|
||
} else {
|
||
_focusNode.removeListener(_onFocusChanged);
|
||
}
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void didUpdateWidget(covariant CurrencyInputField oldWidget) {
|
||
super.didUpdateWidget(oldWidget);
|
||
if (oldWidget.currency != widget.currency) {
|
||
// 통화 변경 시 빌드 이후에 안전하게 재포맷 적용
|
||
if (_focusNode.hasFocus) return;
|
||
final value = _parseValue(widget.controller.text);
|
||
if (value == null) return;
|
||
final formatted = _formatCurrency(value);
|
||
if (widget.controller.text == formatted || _isPostFrameUpdating) return;
|
||
_isPostFrameUpdating = true;
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (!mounted) return;
|
||
widget.controller.value = TextEditingValue(
|
||
text: formatted,
|
||
selection: TextSelection.collapsed(offset: formatted.length),
|
||
);
|
||
_isFormatted = true;
|
||
_isPostFrameUpdating = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
void _onFocusChanged() {
|
||
if (!_focusNode.hasFocus && widget.controller.text.isNotEmpty) {
|
||
// 포커스를 잃었을 때 포맷팅 적용
|
||
final value = _parseValue(widget.controller.text);
|
||
if (value != null) {
|
||
setState(() {
|
||
widget.controller.text = _formatCurrency(value);
|
||
_isFormatted = true;
|
||
});
|
||
}
|
||
} else if (_focusNode.hasFocus && _isFormatted) {
|
||
// 포커스를 받았을 때 포맷팅 제거
|
||
final value = _parseValue(widget.controller.text);
|
||
if (value != null) {
|
||
setState(() {
|
||
if (_isIntegerCurrency(widget.currency)) {
|
||
widget.controller.text = value.toInt().toString();
|
||
} else {
|
||
widget.controller.text = value.toString();
|
||
}
|
||
_isFormatted = false;
|
||
});
|
||
// 커서를 끝으로 이동
|
||
widget.controller.selection = TextSelection.fromPosition(
|
||
TextPosition(offset: widget.controller.text.length),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
String _formatCurrency(double value) {
|
||
if (_isIntegerCurrency(widget.currency)) {
|
||
return NumberFormat.decimalPattern().format(value.toInt());
|
||
} else {
|
||
return NumberFormat('#,##0.00').format(value);
|
||
}
|
||
}
|
||
|
||
double? _parseValue(String text) {
|
||
final cleanText = text
|
||
.replaceAll(',', '')
|
||
.replaceAll('₩', '')
|
||
.replaceAll('¥', '')
|
||
.replaceAll('¥', '')
|
||
.replaceAll('\$', '')
|
||
.trim();
|
||
return double.tryParse(cleanText);
|
||
}
|
||
|
||
// ignore: unused_element
|
||
String get _prefixText {
|
||
switch (widget.currency) {
|
||
case 'KRW':
|
||
return '₩ ';
|
||
case 'JPY':
|
||
return '¥ ';
|
||
case 'CNY':
|
||
return '¥ ';
|
||
case 'USD':
|
||
default:
|
||
return '4 ';
|
||
}
|
||
}
|
||
|
||
String _getDefaultHintText(BuildContext context) {
|
||
return AppLocalizations.of(context).enterAmount;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return BaseTextField(
|
||
controller: widget.controller,
|
||
focusNode: _focusNode,
|
||
label: widget.label,
|
||
hintText: widget.hintText ?? _getDefaultHintText(context),
|
||
textInputAction: widget.textInputAction,
|
||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||
inputFormatters: [
|
||
FilteringTextInputFormatter.allow(
|
||
_isIntegerCurrency(widget.currency)
|
||
? RegExp(r'[0-9]')
|
||
: RegExp(r'[0-9.]'),
|
||
),
|
||
if (!_isIntegerCurrency(widget.currency))
|
||
// 소수 통화(USD/CNY): 소수점 이하 2자리 제한
|
||
TextInputFormatter.withFunction((oldValue, newValue) {
|
||
final text = newValue.text;
|
||
if (text.isEmpty) return newValue;
|
||
|
||
final parts = text.split('.');
|
||
if (parts.length > 2) {
|
||
return oldValue; // 소수점이 2개 이상인 경우 거부
|
||
}
|
||
if (parts.length == 2 && parts[1].length > 2) {
|
||
return oldValue; // 소수점 이하 2자 초과 거부
|
||
}
|
||
return newValue;
|
||
}),
|
||
],
|
||
prefixText: _getPrefixText(),
|
||
onEditingComplete: widget.onEditingComplete,
|
||
enabled: widget.enabled,
|
||
onChanged: (value) {
|
||
final parsedValue = _parseValue(value);
|
||
widget.onChanged?.call(parsedValue);
|
||
},
|
||
validator: widget.validator ??
|
||
(value) {
|
||
if (value == null || value.isEmpty) {
|
||
return AppLocalizations.of(context).amountRequired;
|
||
}
|
||
final parsedValue = _parseValue(value);
|
||
if (parsedValue == null || parsedValue <= 0) {
|
||
return AppLocalizations.of(context).invalidAmount;
|
||
}
|
||
return null;
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
bool _isIntegerCurrency(String code) => code == 'KRW' || code == 'JPY';
|
||
|
||
// 안전한 프리픽스 계산 함수(모든 통화 지원)
|
||
String _currencySymbol(String code) {
|
||
switch (code) {
|
||
case 'KRW':
|
||
return '₩';
|
||
case 'JPY':
|
||
case 'CNY':
|
||
return '¥';
|
||
case 'USD':
|
||
default:
|
||
return '\$';
|
||
}
|
||
}
|
||
|
||
extension on _CurrencyInputFieldState {
|
||
String _getPrefixText() => '${_currencySymbol(widget.currency)} ';
|
||
}
|