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 createState() => _CurrencyInputFieldState(); } class _CurrencyInputFieldState extends State { 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)} '; }