feat: adopt material 3 theme and billing adjustments

This commit is contained in:
JiWoong Sul
2025-09-16 14:30:14 +09:00
parent a01d9092ba
commit 44850a53cc
85 changed files with 2957 additions and 2776 deletions

View File

@@ -5,10 +5,10 @@ import 'base_text_field.dart';
import '../../../l10n/app_localizations.dart';
/// 통화 입력 필드 위젯
/// 원화(KRW)와 달러(USD)를 지원하며 자동 포맷팅을 제공합니다.
/// KRW/JPY(정수), USD/CNY(소수점 2자리)를 지원하며 자동 포맷팅을 제공합니다.
class CurrencyInputField extends StatefulWidget {
final TextEditingController controller;
final String currency; // 'KRW' or 'USD'
final String currency; // 'KRW' | 'USD' | 'JPY' | 'CNY'
final String? label;
final String? hintText;
final Function(double?)? onChanged;
@@ -39,6 +39,7 @@ class CurrencyInputField extends StatefulWidget {
class _CurrencyInputFieldState extends State<CurrencyInputField> {
late FocusNode _focusNode;
bool _isFormatted = false;
bool _isPostFrameUpdating = false;
@override
void initState() {
@@ -66,6 +67,29 @@ class _CurrencyInputFieldState extends State<CurrencyInputField> {
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) {
// 포커스를 잃었을 때 포맷팅 적용
@@ -81,7 +105,7 @@ class _CurrencyInputFieldState extends State<CurrencyInputField> {
final value = _parseValue(widget.controller.text);
if (value != null) {
setState(() {
if (widget.currency == 'KRW') {
if (_isIntegerCurrency(widget.currency)) {
widget.controller.text = value.toInt().toString();
} else {
widget.controller.text = value.toString();
@@ -97,7 +121,7 @@ class _CurrencyInputFieldState extends State<CurrencyInputField> {
}
String _formatCurrency(double value) {
if (widget.currency == 'KRW') {
if (_isIntegerCurrency(widget.currency)) {
return NumberFormat.decimalPattern().format(value.toInt());
} else {
return NumberFormat('#,##0.00').format(value);
@@ -108,13 +132,26 @@ class _CurrencyInputFieldState extends State<CurrencyInputField> {
final cleanText = text
.replaceAll(',', '')
.replaceAll('', '')
.replaceAll('¥', '')
.replaceAll('', '')
.replaceAll('\$', '')
.trim();
return double.tryParse(cleanText);
}
// ignore: unused_element
String get _prefixText {
return widget.currency == 'KRW' ? '' : '\$ ';
switch (widget.currency) {
case 'KRW':
return '';
case 'JPY':
return '¥ ';
case 'CNY':
return '¥ ';
case 'USD':
default:
return '4 ';
}
}
String _getDefaultHintText(BuildContext context) {
@@ -132,26 +169,27 @@ class _CurrencyInputFieldState extends State<CurrencyInputField> {
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(
widget.currency == 'KRW' ? RegExp(r'[0-9]') : RegExp(r'[0-9.]')),
if (widget.currency == 'USD')
// USD의 경우 소수점 이하 2자리까지만 허용
_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) {
// 소수점이 2개 이상인 경우 거부
return oldValue;
return oldValue; // 소수점이 2개 이상인 경우 거부
}
if (parts.length == 2 && parts[1].length > 2) {
// 소수점 이하 2자리를 초과하는 경우 거부
return oldValue;
return oldValue; // 소수점 이하 2자 초과 거부
}
return newValue;
}),
],
prefixText: _prefixText,
prefixText: _getPrefixText(),
onEditingComplete: widget.onEditingComplete,
enabled: widget.enabled,
onChanged: (value) {
@@ -172,3 +210,23 @@ class _CurrencyInputFieldState extends State<CurrencyInputField> {
);
}
}
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)} ';
}